@0xsequence/anypay-sdk
Version:
SDK for Anypay functionality
515 lines (458 loc) • 14.4 kB
text/typescript
import type {
CommitIntentConfigReturn,
GetIntentCallsPayloadsArgs,
GetIntentCallsPayloadsReturn as GetIntentCallsPayloadsReturnFromAPI,
IntentPrecondition,
SequenceAPIClient,
} from "@0xsequence/anypay-api"
import type { Context as ContextLike } from "@0xsequence/wallet-primitives"
import { Config, type Context, Payload } from "@0xsequence/wallet-primitives"
import {
AbiParameters,
Address,
Bytes,
ContractAddress,
Hash,
type Hex,
} from "ox"
import {
type Account,
type Chain,
isAddressEqual,
type WalletClient,
} from "viem"
import { ANYPAY_LIFI_SAPIENT_SIGNER_LITE_ADDRESS } from "./constants.js"
import { findPreconditionAddress } from "./preconditions.js"
export interface AnypayLifiInfo {
originToken: Address.Address
amount: bigint
originChainId: bigint
destinationChainId: bigint
}
export interface IntentCallsPayload extends Payload.Calls {
chainId: bigint
}
export interface MetaTxnFeeDetail {
metaTxnID: string
estimatedGasLimit: string
feeNative: string
}
export interface ChainExecuteQuote {
chainId: string
totalGasLimit: string
gasPrice: string
totalFeeAmount: string
nativeTokenSymbol: string
nativeTokenPrice?: string
metaTxnFeeDetails: Array<MetaTxnFeeDetail>
totalFeeUSD?: string
}
export interface ExecuteQuote {
chainQuotes: Array<ChainExecuteQuote>
}
export interface CrossChainFee {
lifiFee: string
anypaySwapFee: string
lifiFeeUSD: number
anypaySwapFeeUSD: number
totalFeeAmount: string
totalFeeUSD: number
}
export interface AnypayFee {
executeQuote: ExecuteQuote
crossChainFee?: CrossChainFee
anypayFixedFeeUSD?: number
feeToken?: string
originTokenTotalAmount?: string
totalFeeAmount?: string
totalFeeUSD?: string
}
export type GetIntentCallsPayloadsReturn = GetIntentCallsPayloadsReturnFromAPI
export type OriginCallParams = {
to: `0x${string}` | null
data: Hex.Hex | null
value: bigint | null
chainId: number | null
error?: string
}
export type SendOriginCallTxArgs = {
to: string
data: Hex.Hex
value: bigint
chain: Chain
}
export async function getIntentCallsPayloads(
apiClient: SequenceAPIClient,
args: GetIntentCallsPayloadsArgs,
): Promise<GetIntentCallsPayloadsReturn> {
return apiClient.getIntentCallsPayloads(args as any) // TODO: Add proper type
}
export function calculateIntentAddress(
mainSigner: string,
calls: IntentCallsPayload[],
lifiInfosArg: AnypayLifiInfo[] | null | undefined,
): `0x${string}` {
console.log("calculateIntentAddress inputs:", {
mainSigner,
calls: JSON.stringify(calls, null, 2),
lifiInfosArg: JSON.stringify(lifiInfosArg, null, 2),
})
const context: ContextLike.Context = {
factory: "0xBd0F8abD58B4449B39C57Ac9D5C67433239aC447" as `0x${string}`,
stage1: "0x53bA242E7C2501839DF2972c75075dc693176Cd0" as `0x${string}`,
stage2: "0xa29874c88b8Fd557e42219B04b0CeC693e1712f5" as `0x${string}`,
creationCode:
"0x603e600e3d39601e805130553df33d3d34601c57363d3d373d363d30545af43d82803e903d91601c57fd5bf3" as `0x${string}`,
}
const coreCalls = calls.map((call) => ({
type: "call" as const,
chainId: BigInt(call.chainId),
space: call.space ? BigInt(call.space) : 0n,
nonce: call.nonce ? BigInt(call.nonce) : 0n,
calls: call.calls.map((call) => ({
to: Address.from(call.to),
value: BigInt(call.value || "0"),
data: Bytes.toHex(Bytes.from((call.data as Hex.Hex) || "0x")),
gasLimit: BigInt(call.gasLimit || "0"),
delegateCall: !!call.delegateCall,
onlyFallback: !!call.onlyFallback,
behaviorOnError: (Number(call.behaviorOnError) === 0
? "ignore"
: Number(call.behaviorOnError) === 1
? "revert"
: "abort") as "ignore" | "revert" | "abort",
})),
}))
//console.log('Transformed coreCalls:', JSON.stringify(coreCalls, null, 2))
const coreLifiInfos = lifiInfosArg?.map((info: AnypayLifiInfo) => ({
originToken: Address.from(info.originToken),
amount: BigInt(info.amount),
originChainId: BigInt(info.originChainId),
destinationChainId: BigInt(info.destinationChainId),
}))
console.log(
"Transformed coreLifiInfos:",
JSON.stringify(
coreLifiInfos,
(_, v) => (typeof v === "bigint" ? v.toString() : v),
2,
),
)
const calculatedAddress = calculateIntentConfigurationAddress(
Address.from(mainSigner),
coreCalls,
context,
// AnyPay.ANYPAY_LIFI_ATTESATION_SIGNER_ADDRESS,
Address.from("0x0000000000000000000000000000000000000001"),
coreLifiInfos,
)
console.log("Final calculated address:", calculatedAddress.toString())
return calculatedAddress
}
export function commitIntentConfig(
apiClient: SequenceAPIClient,
mainSigner: string,
calls: IntentCallsPayload[],
preconditions: IntentPrecondition[],
lifiInfos: AnypayLifiInfo[],
): Promise<CommitIntentConfigReturn> {
console.log("commitIntentConfig inputs:", {
mainSigner,
calls: JSON.stringify(calls, null, 2),
preconditions: JSON.stringify(preconditions, null, 2),
lifiInfos: JSON.stringify(lifiInfos, null, 2),
})
const calculatedAddress = calculateIntentAddress(mainSigner, calls, lifiInfos)
const receivedAddress = findPreconditionAddress(preconditions)
console.log("Address comparison:", {
receivedAddress,
calculatedAddress: calculatedAddress.toString(),
match: isAddressEqual(Address.from(receivedAddress), calculatedAddress),
})
const args = {
walletAddress: calculatedAddress.toString(),
mainSigner: mainSigner,
calls: calls,
preconditions: preconditions,
lifiInfos: lifiInfos,
}
console.log("args", args)
return apiClient.commitIntentConfig(args as any) // TODO: Add proper type
}
export async function sendOriginTransaction(
wallet: Account,
client: WalletClient,
originParams: SendOriginCallTxArgs,
): Promise<`0x${string}`> {
const chainId = await client.getChainId()
if (chainId.toString() !== originParams.chain.id.toString()) {
console.log(
"sendOriginTransaction: switching chain",
"want:",
originParams.chain.id,
"current:",
chainId,
)
await client.switchChain({ id: originParams.chain.id })
console.log(
"sendOriginTransaction: switched chain to",
originParams.chain.id,
)
}
const hash = await client.sendTransaction({
account: wallet,
to: originParams.to as `0x${string}`,
data: originParams.data as `0x${string}`,
value: BigInt(originParams.value),
chain: originParams.chain,
})
return hash
}
export interface OriginTokenParam {
address: Address.Address
chainId: bigint
}
export interface DestinationTokenParam {
address: Address.Address
chainId: bigint
amount: bigint
}
export function hashIntentParams(params: {
userAddress: Address.Address
nonce: bigint
originTokens: OriginTokenParam[]
destinationCalls: Array<IntentCallsPayload>
destinationTokens: DestinationTokenParam[]
}): string {
if (!params) throw new Error("params is nil")
if (
!params.userAddress ||
params.userAddress === "0x0000000000000000000000000000000000000000"
)
throw new Error("UserAddress is zero")
if (typeof params.nonce !== "bigint") throw new Error("Nonce is not a bigint")
if (!params.originTokens || params.originTokens.length === 0)
throw new Error("OriginTokens is empty")
if (!params.destinationCalls || params.destinationCalls.length === 0)
throw new Error("DestinationCalls is empty")
if (!params.destinationTokens || params.destinationTokens.length === 0)
throw new Error("DestinationTokens is empty")
for (let i = 0; i < params.destinationCalls.length; i++) {
const currentCall = params.destinationCalls[i]
if (!currentCall) throw new Error(`DestinationCalls[${i}] is nil`)
if (!currentCall.calls || currentCall.calls.length === 0) {
throw new Error(`DestinationCalls[${i}] has no calls`)
}
}
const originTokensForAbi = params.originTokens.map((token) => ({
address: token.address,
chainId: token.chainId,
}))
let cumulativeCallsHashBytes: Bytes.Bytes = Bytes.from(new Uint8Array(32))
for (let i = 0; i < params.destinationCalls.length; i++) {
const callPayload = params.destinationCalls[i]!
const currentDestCallPayloadHashBytes = Payload.hash(
Address.from("0x0000000000000000000000000000000000000000"),
callPayload.chainId,
callPayload,
)
cumulativeCallsHashBytes = Hash.keccak256(
Bytes.concat(cumulativeCallsHashBytes, currentDestCallPayloadHashBytes),
{
as: "Bytes",
},
)
}
const cumulativeCallsHashHex = Bytes.toHex(cumulativeCallsHashBytes)
const destinationTokensForAbi = params.destinationTokens.map((token) => ({
address: token.address,
chainId: token.chainId,
amount: token.amount,
}))
const abiSchema = [
{ type: "address" },
{ type: "uint256" },
{
type: "tuple[]",
components: [
{ name: "address", type: "address" },
{ name: "chainId", type: "uint256" },
],
},
{
type: "tuple[]",
components: [
{ name: "address", type: "address" },
{ name: "chainId", type: "uint256" },
{ name: "amount", type: "uint256" },
],
},
{ type: "bytes32" },
]
const encodedHex = AbiParameters.encode(abiSchema, [
params.userAddress,
params.nonce,
originTokensForAbi,
destinationTokensForAbi,
cumulativeCallsHashHex,
]) as Hex.Hex
const encodedBytes = Bytes.fromHex(encodedHex)
const hashBytes = Hash.keccak256(encodedBytes)
const hashHex = Bytes.toHex(hashBytes)
return hashHex
}
// TODO: Add proper type
export function bigintReplacer(_key: string, value: any) {
return typeof value === "bigint" ? value.toString() : value
}
export function getAnypayLifiInfoHash(
lifiInfos: AnypayLifiInfo[],
attestationAddress: Address.Address,
): Hex.Hex {
if (!lifiInfos || lifiInfos.length === 0) {
throw new Error("lifiInfos is empty")
}
if (
!attestationAddress ||
attestationAddress === "0x0000000000000000000000000000000000000000"
) {
throw new Error("attestationAddress is zero")
}
const anypayLifiInfoComponents = [
{ name: "originToken", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "originChainId", type: "uint256" },
{ name: "destinationChainId", type: "uint256" },
]
const lifiInfosForAbi = lifiInfos.map((info) => ({
originToken: info.originToken,
amount: info.amount,
originChainId: info.originChainId,
destinationChainId: info.destinationChainId,
}))
const abiSchema = [
{
type: "tuple[]",
name: "lifiInfos",
components: anypayLifiInfoComponents,
},
{ type: "address", name: "attestationAddress" },
]
const encodedHex = AbiParameters.encode(abiSchema, [
lifiInfosForAbi,
attestationAddress,
]) as Hex.Hex
const encodedBytes = Bytes.fromHex(encodedHex)
const hashBytes = Hash.keccak256(encodedBytes)
return Bytes.toHex(hashBytes)
}
export function calculateIntentConfigurationAddress(
mainSigner: Address.Address,
calls: IntentCallsPayload[],
context: Context.Context,
attestationSigner?: Address.Address,
lifiInfos?: AnypayLifiInfo[],
): Address.Address {
const config = createIntentConfiguration(
mainSigner,
calls,
attestationSigner,
lifiInfos,
)
// Calculate the image hash of the configuration
const imageHash = Config.hashConfiguration(config)
// Calculate the counterfactual address using the image hash and context
return ContractAddress.fromCreate2({
from: context.factory,
bytecodeHash: Hash.keccak256(
Bytes.concat(
Bytes.from(context.creationCode),
Bytes.padLeft(Bytes.from(context.stage1), 32),
),
{ as: "Bytes" },
),
salt: imageHash,
})
}
function createIntentConfiguration(
mainSigner: Address.Address,
calls: IntentCallsPayload[],
attestationSigner?: Address.Address,
lifiInfos?: AnypayLifiInfo[],
): Config.Config {
const mainSignerLeaf: Config.SignerLeaf = {
type: "signer",
address: mainSigner,
weight: 1n,
}
const subdigestLeaves: Config.AnyAddressSubdigestLeaf[] = calls.map(
(call) => {
const digest = Payload.hash(
Address.from("0x0000000000000000000000000000000000000000"),
call.chainId,
call,
)
console.log("digest:", Bytes.toHex(digest))
return {
type: "any-address-subdigest",
digest: Bytes.toHex(digest),
} as Config.AnyAddressSubdigestLeaf
},
)
const otherLeaves: Config.Topology[] = [...subdigestLeaves]
if (lifiInfos && lifiInfos.length > 0) {
if (attestationSigner) {
const lifiConditionLeaf: Config.SapientSignerLeaf = {
type: "sapient-signer",
// address: ANYPAY_LIFI_SAPIENT_SIGNER_ADDRESS,
address: ANYPAY_LIFI_SAPIENT_SIGNER_LITE_ADDRESS,
weight: 1n,
imageHash: getAnypayLifiInfoHash(lifiInfos, attestationSigner),
}
otherLeaves.push(lifiConditionLeaf)
}
}
if (otherLeaves.length === 0) {
throw new Error(
"Intent configuration must have at least one call or LiFi information.",
)
}
let secondaryTopologyNode: Config.Topology
if (otherLeaves.length === 1) {
secondaryTopologyNode = otherLeaves[0]!
} else {
secondaryTopologyNode = buildMerkleTreeFromMembers(otherLeaves)
}
return {
threshold: 1n,
checkpoint: 0n,
topology: [mainSignerLeaf, secondaryTopologyNode] as Config.Node,
}
}
// Renamed and generalized from createSubdigestTree
function buildMerkleTreeFromMembers(
members: Config.Topology[],
): Config.Topology {
if (members.length === 0) {
throw new Error("Cannot create a tree from empty members")
}
if (members.length === 1) {
return members[0]! // Returns a single Leaf or a Node
}
let currentLevel = [...members]
while (currentLevel.length > 1) {
const nextLevel: Config.Topology[] = []
for (let i = 0; i < currentLevel.length; i += 2) {
const left = currentLevel[i]!
if (i + 1 < currentLevel.length) {
const right = currentLevel[i + 1]!
nextLevel.push([left, right] as Config.Node)
} else {
// Odd one out, carries over to the next level
nextLevel.push(left)
}
}
currentLevel = nextLevel
}
return currentLevel[0]!
}