diff --git a/README.md b/README.md index 5e430d2..8dac886 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This project uses SvelteKit and `npm`. It is configured for a deployment to Clou ### Development To start development: + 1. Copy `.env.example` to `.env`. 2. Then fill in the `env` variables by creating a [WalletConnect](https://walletconnect.com) project 3. Also create an [account](https://accounts.polymerlabs.org/) with Polymer to generation [Polymer](https://polymerlabs.org) API keys. diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 0000000..70c933b --- /dev/null +++ b/_typos.toml @@ -0,0 +1,3 @@ +[default.extend-words] +oif = "oif" +OIF = "OIF" \ No newline at end of file diff --git a/src/lib/abi/escrow.ts b/src/lib/abi/escrow.ts index a35f7bf..61a0992 100644 --- a/src/lib/abi/escrow.ts +++ b/src/lib/abi/escrow.ts @@ -1,13 +1,7 @@ export const SETTLER_ESCROW_ABI = [ { type: "constructor", - inputs: [ - { - name: "initialOwner", - type: "address", - internalType: "address" - } - ], + inputs: [], stateMutability: "nonpayable" }, { @@ -23,33 +17,6 @@ export const SETTLER_ESCROW_ABI = [ ], stateMutability: "view" }, - { - type: "function", - name: "applyGovernanceFee", - inputs: [], - outputs: [], - stateMutability: "nonpayable" - }, - { - type: "function", - name: "cancelOwnershipHandover", - inputs: [], - outputs: [], - stateMutability: "payable" - }, - { - type: "function", - name: "completeOwnershipHandover", - inputs: [ - { - name: "pendingOwner", - type: "address", - internalType: "address" - } - ], - outputs: [], - stateMutability: "payable" - }, { type: "function", name: "eip712Domain", @@ -346,45 +313,6 @@ export const SETTLER_ESCROW_ABI = [ outputs: [], stateMutability: "nonpayable" }, - { - type: "function", - name: "governanceFee", - inputs: [], - outputs: [ - { - name: "", - type: "uint64", - internalType: "uint64" - } - ], - stateMutability: "view" - }, - { - type: "function", - name: "nextGovernanceFee", - inputs: [], - outputs: [ - { - name: "", - type: "uint64", - internalType: "uint64" - } - ], - stateMutability: "view" - }, - { - type: "function", - name: "nextGovernanceFeeTime", - inputs: [], - outputs: [ - { - name: "", - type: "uint64", - internalType: "uint64" - } - ], - stateMutability: "view" - }, { type: "function", name: "open", @@ -828,38 +756,6 @@ export const SETTLER_ESCROW_ABI = [ ], stateMutability: "view" }, - { - type: "function", - name: "owner", - inputs: [], - outputs: [ - { - name: "result", - type: "address", - internalType: "address" - } - ], - stateMutability: "view" - }, - { - type: "function", - name: "ownershipHandoverExpiresAt", - inputs: [ - { - name: "pendingOwner", - type: "address", - internalType: "address" - } - ], - outputs: [ - { - name: "result", - type: "uint256", - internalType: "uint256" - } - ], - stateMutability: "view" - }, { type: "function", name: "purchaseOrder", @@ -1135,46 +1031,6 @@ export const SETTLER_ESCROW_ABI = [ outputs: [], stateMutability: "nonpayable" }, - { - type: "function", - name: "renounceOwnership", - inputs: [], - outputs: [], - stateMutability: "payable" - }, - { - type: "function", - name: "requestOwnershipHandover", - inputs: [], - outputs: [], - stateMutability: "payable" - }, - { - type: "function", - name: "setGovernanceFee", - inputs: [ - { - name: "_nextGovernanceFee", - type: "uint64", - internalType: "uint64" - } - ], - outputs: [], - stateMutability: "nonpayable" - }, - { - type: "function", - name: "transferOwnership", - inputs: [ - { - name: "newOwner", - type: "address", - internalType: "address" - } - ], - outputs: [], - stateMutability: "payable" - }, { type: "event", name: "EIP712DomainChanged", @@ -1206,44 +1062,6 @@ export const SETTLER_ESCROW_ABI = [ ], anonymous: false }, - { - type: "event", - name: "GovernanceFeeChanged", - inputs: [ - { - name: "oldGovernanceFee", - type: "uint64", - indexed: false, - internalType: "uint64" - }, - { - name: "newGovernanceFee", - type: "uint64", - indexed: false, - internalType: "uint64" - } - ], - anonymous: false - }, - { - type: "event", - name: "NextGovernanceFee", - inputs: [ - { - name: "nextGovernanceFee", - type: "uint64", - indexed: false, - internalType: "uint64" - }, - { - name: "nextGovernanceFeeTime", - type: "uint64", - indexed: false, - internalType: "uint64" - } - ], - anonymous: false - }, { type: "event", name: "Open", @@ -1347,19 +1165,6 @@ export const SETTLER_ESCROW_ABI = [ ], anonymous: false }, - { - type: "event", - name: "Open", - inputs: [ - { - name: "orderId", - type: "bytes32", - indexed: true, - internalType: "bytes32" - } - ], - anonymous: false - }, { type: "event", name: "OrderPurchased", @@ -1385,51 +1190,6 @@ export const SETTLER_ESCROW_ABI = [ ], anonymous: false }, - { - type: "event", - name: "OwnershipHandoverCanceled", - inputs: [ - { - name: "pendingOwner", - type: "address", - indexed: true, - internalType: "address" - } - ], - anonymous: false - }, - { - type: "event", - name: "OwnershipHandoverRequested", - inputs: [ - { - name: "pendingOwner", - type: "address", - indexed: true, - internalType: "address" - } - ], - anonymous: false - }, - { - type: "event", - name: "OwnershipTransferred", - inputs: [ - { - name: "oldOwner", - type: "address", - indexed: true, - internalType: "address" - }, - { - name: "newOwner", - type: "address", - indexed: true, - internalType: "address" - } - ], - anonymous: false - }, { type: "event", name: "Refunded", @@ -1443,11 +1203,6 @@ export const SETTLER_ESCROW_ABI = [ ], anonymous: false }, - { - type: "error", - name: "AlreadyInitialized", - inputs: [] - }, { type: "error", name: "AlreadyPurchased", @@ -1491,15 +1246,15 @@ export const SETTLER_ESCROW_ABI = [ }, { type: "error", - name: "FilledTooLate", + name: "FillDeadlineAfterExpiry", inputs: [ { - name: "expected", + name: "fillDeadline", type: "uint32", internalType: "uint32" }, { - name: "actual", + name: "expires", type: "uint32", internalType: "uint32" } @@ -1545,21 +1300,11 @@ export const SETTLER_ESCROW_ABI = [ name: "InvalidTimestampLength", inputs: [] }, - { - type: "error", - name: "NewOwnerIsZeroAddress", - inputs: [] - }, { type: "error", name: "NoDestination", inputs: [] }, - { - type: "error", - name: "NoHandoverRequest", - inputs: [] - }, { type: "error", name: "NotOrderOwner", @@ -1636,8 +1381,14 @@ export const SETTLER_ESCROW_ABI = [ }, { type: "error", - name: "Unauthorized", - inputs: [] + name: "UnexpectedCaller", + inputs: [ + { + name: "expectedCaller", + type: "bytes32", + internalType: "bytes32" + } + ] }, { type: "error", diff --git a/src/lib/abi/multichain_compact.ts b/src/lib/abi/multichain_compact.ts new file mode 100644 index 0000000..a4f7ef8 --- /dev/null +++ b/src/lib/abi/multichain_compact.ts @@ -0,0 +1,624 @@ +export const MULTICHAIN_SETTLER_COMPACT_ABI = [ + { + type: "constructor", + inputs: [ + { + name: "compact", + type: "address", + internalType: "address" + } + ], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "COMPACT", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "contract TheCompact" + } + ], + stateMutability: "view" + }, + { + type: "function", + name: "DOMAIN_SEPARATOR", + inputs: [], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32" + } + ], + stateMutability: "view" + }, + { + type: "function", + name: "eip712Domain", + inputs: [], + outputs: [ + { + name: "fields", + type: "bytes1", + internalType: "bytes1" + }, + { + name: "name", + type: "string", + internalType: "string" + }, + { + name: "version", + type: "string", + internalType: "string" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "verifyingContract", + type: "address", + internalType: "address" + }, + { + name: "salt", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "extensions", + type: "uint256[]", + internalType: "uint256[]" + } + ], + stateMutability: "view" + }, + { + type: "function", + name: "finalise", + inputs: [ + { + name: "order", + type: "tuple", + internalType: "struct MultichainOrderComponent", + components: [ + { + name: "user", + type: "address", + internalType: "address" + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIdField", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIndex", + type: "uint256", + internalType: "uint256" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + }, + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "inputOracle", + type: "address", + internalType: "address" + }, + { + name: "inputs", + type: "uint256[2][]", + internalType: "uint256[2][]" + }, + { + name: "outputs", + type: "tuple[]", + internalType: "struct MandateOutput[]", + components: [ + { + name: "oracle", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "settler", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "token", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "amount", + type: "uint256", + internalType: "uint256" + }, + { + name: "recipient", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "callbackData", + type: "bytes", + internalType: "bytes" + }, + { + name: "context", + type: "bytes", + internalType: "bytes" + } + ] + }, + { + name: "additionalChains", + type: "bytes32[]", + internalType: "bytes32[]" + } + ] + }, + { + name: "signatures", + type: "bytes", + internalType: "bytes" + }, + { + name: "solveParams", + type: "tuple[]", + internalType: "struct InputSettlerBase.SolveParams[]", + components: [ + { + name: "timestamp", + type: "uint32", + internalType: "uint32" + }, + { + name: "solver", + type: "bytes32", + internalType: "bytes32" + } + ] + }, + { + name: "destination", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "call", + type: "bytes", + internalType: "bytes" + } + ], + outputs: [], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "finaliseWithSignature", + inputs: [ + { + name: "order", + type: "tuple", + internalType: "struct MultichainOrderComponent", + components: [ + { + name: "user", + type: "address", + internalType: "address" + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIdField", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIndex", + type: "uint256", + internalType: "uint256" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + }, + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "inputOracle", + type: "address", + internalType: "address" + }, + { + name: "inputs", + type: "uint256[2][]", + internalType: "uint256[2][]" + }, + { + name: "outputs", + type: "tuple[]", + internalType: "struct MandateOutput[]", + components: [ + { + name: "oracle", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "settler", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "token", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "amount", + type: "uint256", + internalType: "uint256" + }, + { + name: "recipient", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "callbackData", + type: "bytes", + internalType: "bytes" + }, + { + name: "context", + type: "bytes", + internalType: "bytes" + } + ] + }, + { + name: "additionalChains", + type: "bytes32[]", + internalType: "bytes32[]" + } + ] + }, + { + name: "signatures", + type: "bytes", + internalType: "bytes" + }, + { + name: "solveParams", + type: "tuple[]", + internalType: "struct InputSettlerBase.SolveParams[]", + components: [ + { + name: "timestamp", + type: "uint32", + internalType: "uint32" + }, + { + name: "solver", + type: "bytes32", + internalType: "bytes32" + } + ] + }, + { + name: "destination", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "call", + type: "bytes", + internalType: "bytes" + }, + { + name: "orderOwnerSignature", + type: "bytes", + internalType: "bytes" + } + ], + outputs: [], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "orderIdentifier", + inputs: [ + { + name: "order", + type: "tuple", + internalType: "struct MultichainOrderComponent", + components: [ + { + name: "user", + type: "address", + internalType: "address" + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIdField", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIndex", + type: "uint256", + internalType: "uint256" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + }, + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "inputOracle", + type: "address", + internalType: "address" + }, + { + name: "inputs", + type: "uint256[2][]", + internalType: "uint256[2][]" + }, + { + name: "outputs", + type: "tuple[]", + internalType: "struct MandateOutput[]", + components: [ + { + name: "oracle", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "settler", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "token", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "amount", + type: "uint256", + internalType: "uint256" + }, + { + name: "recipient", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "callbackData", + type: "bytes", + internalType: "bytes" + }, + { + name: "context", + type: "bytes", + internalType: "bytes" + } + ] + }, + { + name: "additionalChains", + type: "bytes32[]", + internalType: "bytes32[]" + } + ] + } + ], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32" + } + ], + stateMutability: "view" + }, + { + type: "event", + name: "EIP712DomainChanged", + inputs: [], + anonymous: false + }, + { + type: "event", + name: "Finalised", + inputs: [ + { + name: "orderId", + type: "bytes32", + indexed: true, + internalType: "bytes32" + }, + { + name: "solver", + type: "bytes32", + indexed: false, + internalType: "bytes32" + }, + { + name: "destination", + type: "bytes32", + indexed: false, + internalType: "bytes32" + } + ], + anonymous: false + }, + { + type: "error", + name: "CallOutOfRange", + inputs: [] + }, + { + type: "error", + name: "ContextOutOfRange", + inputs: [] + }, + { + type: "error", + name: "FillDeadlineAfterExpiry", + inputs: [ + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + } + ] + }, + { + type: "error", + name: "FilledTooLate", + inputs: [ + { + name: "expected", + type: "uint32", + internalType: "uint32" + }, + { + name: "actual", + type: "uint32", + internalType: "uint32" + } + ] + }, + { + type: "error", + name: "InvalidShortString", + inputs: [] + }, + { + type: "error", + name: "InvalidSigner", + inputs: [] + }, + { + type: "error", + name: "InvalidTimestampLength", + inputs: [] + }, + { + type: "error", + name: "NoDestination", + inputs: [] + }, + { + type: "error", + name: "StringTooLong", + inputs: [ + { + name: "str", + type: "string", + internalType: "string" + } + ] + }, + { + type: "error", + name: "TimestampNotPassed", + inputs: [] + }, + { + type: "error", + name: "TimestampPassed", + inputs: [] + }, + { + type: "error", + name: "UnexpectedCaller", + inputs: [ + { + name: "expectedCaller", + type: "bytes32", + internalType: "bytes32" + } + ] + }, + { + type: "error", + name: "UserCannotBeSettler", + inputs: [] + }, + { + type: "error", + name: "WrongChain", + inputs: [ + { + name: "expected", + type: "uint256", + internalType: "uint256" + }, + { + name: "actual", + type: "uint256", + internalType: "uint256" + } + ] + } +] as const; diff --git a/src/lib/abi/multichain_escrow.ts b/src/lib/abi/multichain_escrow.ts new file mode 100644 index 0000000..892b962 --- /dev/null +++ b/src/lib/abi/multichain_escrow.ts @@ -0,0 +1,1051 @@ +export const MULTICHAIN_SETTLER_ESCROW_ABI = [ + { + type: "constructor", + inputs: [], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "DOMAIN_SEPARATOR", + inputs: [], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32" + } + ], + stateMutability: "view" + }, + { + type: "function", + name: "eip712Domain", + inputs: [], + outputs: [ + { + name: "fields", + type: "bytes1", + internalType: "bytes1" + }, + { + name: "name", + type: "string", + internalType: "string" + }, + { + name: "version", + type: "string", + internalType: "string" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "verifyingContract", + type: "address", + internalType: "address" + }, + { + name: "salt", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "extensions", + type: "uint256[]", + internalType: "uint256[]" + } + ], + stateMutability: "view" + }, + { + type: "function", + name: "finalise", + inputs: [ + { + name: "order", + type: "tuple", + internalType: "struct MultichainOrderComponent", + components: [ + { + name: "user", + type: "address", + internalType: "address" + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIdField", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIndex", + type: "uint256", + internalType: "uint256" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + }, + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "inputOracle", + type: "address", + internalType: "address" + }, + { + name: "inputs", + type: "uint256[2][]", + internalType: "uint256[2][]" + }, + { + name: "outputs", + type: "tuple[]", + internalType: "struct MandateOutput[]", + components: [ + { + name: "oracle", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "settler", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "token", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "amount", + type: "uint256", + internalType: "uint256" + }, + { + name: "recipient", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "callbackData", + type: "bytes", + internalType: "bytes" + }, + { + name: "context", + type: "bytes", + internalType: "bytes" + } + ] + }, + { + name: "additionalChains", + type: "bytes32[]", + internalType: "bytes32[]" + } + ] + }, + { + name: "solveParams", + type: "tuple[]", + internalType: "struct InputSettlerBase.SolveParams[]", + components: [ + { + name: "timestamp", + type: "uint32", + internalType: "uint32" + }, + { + name: "solver", + type: "bytes32", + internalType: "bytes32" + } + ] + }, + { + name: "destination", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "call", + type: "bytes", + internalType: "bytes" + } + ], + outputs: [], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "finaliseWithSignature", + inputs: [ + { + name: "order", + type: "tuple", + internalType: "struct MultichainOrderComponent", + components: [ + { + name: "user", + type: "address", + internalType: "address" + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIdField", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIndex", + type: "uint256", + internalType: "uint256" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + }, + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "inputOracle", + type: "address", + internalType: "address" + }, + { + name: "inputs", + type: "uint256[2][]", + internalType: "uint256[2][]" + }, + { + name: "outputs", + type: "tuple[]", + internalType: "struct MandateOutput[]", + components: [ + { + name: "oracle", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "settler", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "token", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "amount", + type: "uint256", + internalType: "uint256" + }, + { + name: "recipient", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "callbackData", + type: "bytes", + internalType: "bytes" + }, + { + name: "context", + type: "bytes", + internalType: "bytes" + } + ] + }, + { + name: "additionalChains", + type: "bytes32[]", + internalType: "bytes32[]" + } + ] + }, + { + name: "solveParams", + type: "tuple[]", + internalType: "struct InputSettlerBase.SolveParams[]", + components: [ + { + name: "timestamp", + type: "uint32", + internalType: "uint32" + }, + { + name: "solver", + type: "bytes32", + internalType: "bytes32" + } + ] + }, + { + name: "destination", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "call", + type: "bytes", + internalType: "bytes" + }, + { + name: "orderOwnerSignature", + type: "bytes", + internalType: "bytes" + } + ], + outputs: [], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "open", + inputs: [ + { + name: "order", + type: "tuple", + internalType: "struct MultichainOrderComponent", + components: [ + { + name: "user", + type: "address", + internalType: "address" + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIdField", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIndex", + type: "uint256", + internalType: "uint256" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + }, + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "inputOracle", + type: "address", + internalType: "address" + }, + { + name: "inputs", + type: "uint256[2][]", + internalType: "uint256[2][]" + }, + { + name: "outputs", + type: "tuple[]", + internalType: "struct MandateOutput[]", + components: [ + { + name: "oracle", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "settler", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "token", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "amount", + type: "uint256", + internalType: "uint256" + }, + { + name: "recipient", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "callbackData", + type: "bytes", + internalType: "bytes" + }, + { + name: "context", + type: "bytes", + internalType: "bytes" + } + ] + }, + { + name: "additionalChains", + type: "bytes32[]", + internalType: "bytes32[]" + } + ] + } + ], + outputs: [], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "openFor", + inputs: [ + { + name: "order", + type: "tuple", + internalType: "struct MultichainOrderComponent", + components: [ + { + name: "user", + type: "address", + internalType: "address" + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIdField", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIndex", + type: "uint256", + internalType: "uint256" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + }, + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "inputOracle", + type: "address", + internalType: "address" + }, + { + name: "inputs", + type: "uint256[2][]", + internalType: "uint256[2][]" + }, + { + name: "outputs", + type: "tuple[]", + internalType: "struct MandateOutput[]", + components: [ + { + name: "oracle", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "settler", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "token", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "amount", + type: "uint256", + internalType: "uint256" + }, + { + name: "recipient", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "callbackData", + type: "bytes", + internalType: "bytes" + }, + { + name: "context", + type: "bytes", + internalType: "bytes" + } + ] + }, + { + name: "additionalChains", + type: "bytes32[]", + internalType: "bytes32[]" + } + ] + }, + { + name: "sponsor", + type: "address", + internalType: "address" + }, + { + name: "signature", + type: "bytes", + internalType: "bytes" + } + ], + outputs: [], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "orderIdentifier", + inputs: [ + { + name: "order", + type: "tuple", + internalType: "struct MultichainOrderComponent", + components: [ + { + name: "user", + type: "address", + internalType: "address" + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIdField", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIndex", + type: "uint256", + internalType: "uint256" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + }, + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "inputOracle", + type: "address", + internalType: "address" + }, + { + name: "inputs", + type: "uint256[2][]", + internalType: "uint256[2][]" + }, + { + name: "outputs", + type: "tuple[]", + internalType: "struct MandateOutput[]", + components: [ + { + name: "oracle", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "settler", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "token", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "amount", + type: "uint256", + internalType: "uint256" + }, + { + name: "recipient", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "callbackData", + type: "bytes", + internalType: "bytes" + }, + { + name: "context", + type: "bytes", + internalType: "bytes" + } + ] + }, + { + name: "additionalChains", + type: "bytes32[]", + internalType: "bytes32[]" + } + ] + } + ], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32" + } + ], + stateMutability: "view" + }, + { + type: "function", + name: "orderStatus", + inputs: [ + { + name: "orderId", + type: "bytes32", + internalType: "bytes32" + } + ], + outputs: [ + { + name: "", + type: "uint8", + internalType: "enum InputSettlerMultichainEscrow.OrderStatus" + } + ], + stateMutability: "view" + }, + { + type: "function", + name: "refund", + inputs: [ + { + name: "order", + type: "tuple", + internalType: "struct MultichainOrderComponent", + components: [ + { + name: "user", + type: "address", + internalType: "address" + }, + { + name: "nonce", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIdField", + type: "uint256", + internalType: "uint256" + }, + { + name: "chainIndex", + type: "uint256", + internalType: "uint256" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + }, + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "inputOracle", + type: "address", + internalType: "address" + }, + { + name: "inputs", + type: "uint256[2][]", + internalType: "uint256[2][]" + }, + { + name: "outputs", + type: "tuple[]", + internalType: "struct MandateOutput[]", + components: [ + { + name: "oracle", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "settler", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "chainId", + type: "uint256", + internalType: "uint256" + }, + { + name: "token", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "amount", + type: "uint256", + internalType: "uint256" + }, + { + name: "recipient", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "callbackData", + type: "bytes", + internalType: "bytes" + }, + { + name: "context", + type: "bytes", + internalType: "bytes" + } + ] + }, + { + name: "additionalChains", + type: "bytes32[]", + internalType: "bytes32[]" + } + ] + } + ], + outputs: [], + stateMutability: "nonpayable" + }, + { + type: "event", + name: "EIP712DomainChanged", + inputs: [], + anonymous: false + }, + { + type: "event", + name: "Finalised", + inputs: [ + { + name: "orderId", + type: "bytes32", + indexed: true, + internalType: "bytes32" + }, + { + name: "solver", + type: "bytes32", + indexed: false, + internalType: "bytes32" + }, + { + name: "destination", + type: "bytes32", + indexed: false, + internalType: "bytes32" + } + ], + anonymous: false + }, + { + type: "event", + name: "Open", + inputs: [ + { + name: "orderId", + type: "bytes32", + indexed: true, + internalType: "bytes32" + }, + { + name: "order", + type: "bytes", + indexed: false, + internalType: "bytes" + } + ], + anonymous: false + }, + { + type: "event", + name: "Refunded", + inputs: [ + { + name: "orderId", + type: "bytes32", + indexed: true, + internalType: "bytes32" + } + ], + anonymous: false + }, + { + type: "error", + name: "CallOutOfRange", + inputs: [] + }, + { + type: "error", + name: "ChainIndexOutOfRange", + inputs: [ + { + name: "chainIndex", + type: "uint256", + internalType: "uint256" + }, + { + name: "numSegments", + type: "uint256", + internalType: "uint256" + } + ] + }, + { + type: "error", + name: "CodeSize0", + inputs: [] + }, + { + type: "error", + name: "ContextOutOfRange", + inputs: [] + }, + { + type: "error", + name: "FillDeadlineAfterExpiry", + inputs: [ + { + name: "fillDeadline", + type: "uint32", + internalType: "uint32" + }, + { + name: "expires", + type: "uint32", + internalType: "uint32" + } + ] + }, + { + type: "error", + name: "FilledTooLate", + inputs: [ + { + name: "expected", + type: "uint32", + internalType: "uint32" + }, + { + name: "actual", + type: "uint32", + internalType: "uint32" + } + ] + }, + { + type: "error", + name: "HasDirtyBits", + inputs: [] + }, + { + type: "error", + name: "InvalidOrderStatus", + inputs: [] + }, + { + type: "error", + name: "InvalidShortString", + inputs: [] + }, + { + type: "error", + name: "InvalidSigner", + inputs: [] + }, + { + type: "error", + name: "InvalidTimestampLength", + inputs: [] + }, + { + type: "error", + name: "NoDestination", + inputs: [] + }, + { + type: "error", + name: "OrderIdMismatch", + inputs: [ + { + name: "provided", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "computed", + type: "bytes32", + internalType: "bytes32" + } + ] + }, + { + type: "error", + name: "ReentrancyDetected", + inputs: [] + }, + { + type: "error", + name: "SafeERC20FailedOperation", + inputs: [ + { + name: "token", + type: "address", + internalType: "address" + } + ] + }, + { + type: "error", + name: "SignatureAndInputsNotEqual", + inputs: [] + }, + { + type: "error", + name: "SignatureNotSupported", + inputs: [ + { + name: "", + type: "bytes1", + internalType: "bytes1" + } + ] + }, + { + type: "error", + name: "StringTooLong", + inputs: [ + { + name: "str", + type: "string", + internalType: "string" + } + ] + }, + { + type: "error", + name: "TimestampNotPassed", + inputs: [] + }, + { + type: "error", + name: "TimestampPassed", + inputs: [] + }, + { + type: "error", + name: "UnexpectedCaller", + inputs: [ + { + name: "expectedCaller", + type: "bytes32", + internalType: "bytes32" + } + ] + }, + { + type: "error", + name: "WrongChain", + inputs: [ + { + name: "expected", + type: "uint256", + internalType: "uint256" + }, + { + name: "actual", + type: "uint256", + internalType: "uint256" + } + ] + } +] as const; diff --git a/src/lib/abi/outputsettler.ts b/src/lib/abi/outputsettler.ts index 9321129..ac4a130 100644 --- a/src/lib/abi/outputsettler.ts +++ b/src/lib/abi/outputsettler.ts @@ -496,17 +496,6 @@ export const COIN_FILLER_ABI = [ name: "ContextOutOfRange", inputs: [] }, - { - type: "error", - name: "ExclusiveTo", - inputs: [ - { - name: "solver", - type: "bytes32", - internalType: "bytes32" - } - ] - }, { type: "error", name: "FailedCall", @@ -554,11 +543,6 @@ export const COIN_FILLER_ABI = [ } ] }, - { - type: "error", - name: "InvalidContextDataLength", - inputs: [] - }, { type: "error", name: "NotDivisible", @@ -575,11 +559,6 @@ export const COIN_FILLER_ABI = [ } ] }, - { - type: "error", - name: "NotImplemented", - inputs: [] - }, { type: "error", name: "NotProven", @@ -648,10 +627,5 @@ export const COIN_FILLER_ABI = [ internalType: "bytes32" } ] - }, - { - type: "error", - name: "ZeroValue", - inputs: [] } ] as const; diff --git a/src/lib/components/BalanceField.svelte b/src/lib/components/BalanceField.svelte index 3f02899..bde44f0 100644 --- a/src/lib/components/BalanceField.svelte +++ b/src/lib/components/BalanceField.svelte @@ -13,21 +13,21 @@ {#await value} {:then value} {:catch error} diff --git a/src/lib/components/GetQuote.svelte b/src/lib/components/GetQuote.svelte index 35c7509..f8f0fda 100644 --- a/src/lib/components/GetQuote.svelte +++ b/src/lib/components/GetQuote.svelte @@ -1,22 +1,18 @@ -
+
{#await quoteRequest} -
Fetch Quote
+
Fetch Quote
{:then _} {#if quoteExpires !== 0} diff --git a/src/lib/components/InputTokenModal.svelte b/src/lib/components/InputTokenModal.svelte new file mode 100644 index 0000000..e94a117 --- /dev/null +++ b/src/lib/components/InputTokenModal.svelte @@ -0,0 +1,199 @@ + + +
+ + +
+

Select Input

+
+ + + +
+
+
+ {#each tokenSet as tkn} + {tkn.chain} + {/each} +
+
+ {#each tokenSet as tkn} +
+ + of + +
+ {/each} +
+
+ +
+
diff --git a/src/lib/components/Introduction.svelte b/src/lib/components/Introduction.svelte index f9f3582..ea22fea 100644 --- a/src/lib/components/Introduction.svelte +++ b/src/lib/components/Introduction.svelte @@ -4,11 +4,21 @@ Open Intents Framework. It is work in progress and currently support a seamless resource lock flow using + >. It currently support a seamless resource lock flow using The Compact and a traditional escrow flow. + > and a traditional escrow flow, along with a work in progress multichain flow. +

+ +
+ +

Multichain

+

+ A multichain intent is an intent that collects inputs on multiple chains, providing the result + on one or more chains. In other words, a multichain intent is an any to any intent. + Multichain intents are currently work in progress and will break in the future. If you are using + this interface for testing, ensure the multichain flag is not shown.


@@ -57,4 +67,16 @@ >Open Intents Framework.

+ +
+ +

Same Chain

+

+ A same chain intent is an intent that only has inputs and outputs on the same chain. The oracle + is configured different to a cross-chain intent. SetAttestation has to be called on the output + settler to expose the filled output. Learm more about same chain intents or explore a demo of how to collect inputs before delivering outputs. +

diff --git a/src/lib/components/OutputTokenModal.svelte b/src/lib/components/OutputTokenModal.svelte new file mode 100644 index 0000000..f053ad7 --- /dev/null +++ b/src/lib/components/OutputTokenModal.svelte @@ -0,0 +1,131 @@ + + +
+ + +
+

Select Output

+
+
+ {#each outputs as output} +
+ + + +
+ {/each} +
+
+
+ + + +
+
+
diff --git a/src/lib/config.ts b/src/lib/config.ts index fd38387..3efc4ad 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -15,7 +15,11 @@ export const BYTES32_ZERO = export const COMPACT = "0x00000000000000171ede64904551eeDF3C6C9788" as const; export const INPUT_SETTLER_COMPACT_LIFI = "0x0000000000cd5f7fDEc90a03a31F79E5Fbc6A9Cf" as const; export const INPUT_SETTLER_ESCROW_LIFI = "0x000025c3226C00B2Cdc200005a1600509f4e00C0" as const; -export const ALWAYS_OK_ALLOCATOR = "301267367668059890006832136" as const; +export const MULTICHAIN_INPUT_SETTLER_ESCROW = + "0xb912b4c38ab54b94D45Ac001484dEBcbb519Bc2B" as const; +export const MULTICHAIN_INPUT_SETTLER_COMPACT = + "0x1fccC0807F25A58eB531a0B5b4bf3dCE88808Ed7" as const; +export const ALWAYS_OK_ALLOCATOR = "281773970620737143753120258" as const; export const POLYMER_ALLOCATOR = "116450367070547927622991121" as const; // 0x02ecC89C25A5DCB1206053530c58E002a737BD11 signing by 0x934244C8cd6BeBDBd0696A659D77C9BDfE86Efe6 export const COIN_FILLER = "0x0000000000eC36B683C2E6AC89e9A75989C22a2e" as const; export const WORMHOLE_ORACLE = { @@ -43,13 +47,18 @@ export const chainMap = { ethereum, base, arbitrum, - arbitrumSepolia, sepolia, + arbitrumSepolia, optimismSepolia, baseSepolia } as const; export const chains = Object.keys(chainMap) as (keyof typeof chainMap)[]; export type chain = (typeof chains)[number]; +export const chainList = (mainnet: boolean) => { + if (mainnet == true) { + return ["ethereum", "base", "arbitrum"]; + } else return ["sepolia", "optimismSepolia", "baseSepolia", "arbitrumSepolia"]; +}; export type balanceQuery = Record>>; @@ -199,8 +208,8 @@ export function printToken(token: Token) { return `${token.name.toUpperCase()}, ${token.chain}`; } -export function formatTokenAmount(amount: bigint, token: Token, decimals = 4) { - const formattedAmount = Number(amount) / 10 ** token.decimals; +export function formatTokenAmount(amount: bigint, tokenDecimals: number, decimals = 4) { + const formattedAmount = Number(amount) / 10 ** tokenDecimals; return formattedAmount.toFixed(decimals); } diff --git a/src/lib/libraries/assetSelection.ts b/src/lib/libraries/assetSelection.ts new file mode 100644 index 0000000..810ea4b --- /dev/null +++ b/src/lib/libraries/assetSelection.ts @@ -0,0 +1,80 @@ +const bigIntSum = (...nums: bigint[]) => nums.reduce((a, b) => a + b, 0n); + +export class AssetSelection { + goal: bigint; + values: bigint[]; + weights?: bigint[]; + + sortedValues: bigint[]; + + static Sum(values: bigint[]) { + return bigIntSum(...values); + } + + static feasible(goal: bigint, values: bigint[]) { + if (bigIntSum(...values) < goal) + throw Error(`Values makes ${bigIntSum(...values)} cannot sum ${goal}`); + } + + static zip(arr: bigint[]): [bigint, number][] { + return arr.map((v, i) => [v, i]); + } + + static unzip(arr: [bigint, number][]): bigint[] { + arr.sort((a, b) => a[1] - b[1]); + return arr.map((v) => v[0]); + } + + static takeFromArray(goal: bigint, values: [bigint, T][]) { + let sum = 0n; + for (let i = 0; i < values.length; ++i) { + const value = values[i][0]; + const less = goal - sum; + const diff = less < value ? less : value; + sum += diff; + values[i][0] = diff; + } + } + + constructor(opts: { goal: bigint; values: bigint[]; weights?: bigint[] }) { + AssetSelection.feasible(opts.goal, opts.values); + this.goal = opts.goal; + this.values = opts.values; + this.weights = opts.weights; + + this.sortedValues = this.values; + } + + // --- Get sorted values as --- // + + asValues() { + return this.sortedValues; + } + + asIndices() { + const zipped = AssetSelection.zip(this.sortedValues); + return zipped.filter((v) => v[0] > 0); + } + + // --- Sorting Methods --- // + + largest() { + const values = AssetSelection.zip(this.values); + values.sort((a, b) => Number(b[0] - a[0])); + + AssetSelection.takeFromArray(this.goal, values); + + this.sortedValues = AssetSelection.unzip(values); + return this; + } + + smallest() { + const values = AssetSelection.zip(this.values); + values.sort((a, b) => Number(a[0] - b[0])); + + AssetSelection.takeFromArray(this.goal, values); + + this.sortedValues = AssetSelection.unzip(values); + return this; + } +} diff --git a/src/lib/libraries/compactLib.ts b/src/lib/libraries/compactLib.ts index 3383da1..be1786f 100644 --- a/src/lib/libraries/compactLib.ts +++ b/src/lib/libraries/compactLib.ts @@ -13,6 +13,7 @@ import { import { COMPACT_ABI } from "$lib/abi/compact"; import { addressToBytes32 } from "$lib/utils/convert"; import { ERC20_ABI } from "$lib/abi/erc20"; +import type { TokenContext } from "$lib/state.svelte"; export class CompactLib { static compactDeposit( @@ -20,16 +21,16 @@ export class CompactLib { opts: { preHook?: (chain: chain) => Promise; postHook?: () => Promise; - inputToken: Token; + inputToken: TokenContext; account: () => `0x${string}`; - inputAmount: bigint; allocatorId: string; } ) { return async () => { - const { preHook, postHook, inputToken, account, allocatorId, inputAmount } = opts; + const { preHook, postHook, inputToken, account, allocatorId } = opts; + const { token, amount } = inputToken; const publicClients = clients; - if (preHook) await preHook(inputToken.chain); + if (preHook) await preHook(token.chain); const lockTag: `0x${string}` = `0x${toHex( toId(true, ResetPeriod.OneDay, allocatorId, ADDRESS_ZERO), { @@ -41,27 +42,27 @@ export class CompactLib { const recipient = ADDRESS_ZERO; // This means sender. let transactionHash: `0x${string}`; - if (inputToken.address === ADDRESS_ZERO) { + if (token.address === ADDRESS_ZERO) { transactionHash = await walletClient.writeContract({ - chain: chainMap[inputToken.chain], + chain: chainMap[token.chain], account: account(), address: COMPACT, abi: COMPACT_ABI, functionName: "depositNative", - value: inputAmount, + value: amount, args: [lockTag, recipient] }); } else { transactionHash = await walletClient.writeContract({ - chain: chainMap[inputToken.chain], + chain: chainMap[token.chain], account: account(), address: COMPACT, abi: COMPACT_ABI, functionName: "depositERC20", - args: [inputToken.address, lockTag, inputAmount, recipient] + args: [token.address, lockTag, amount, recipient] }); } - await publicClients[inputToken.chain].waitForTransactionReceipt({ + await publicClients[token.chain].waitForTransactionReceipt({ hash: await transactionHash }); if (postHook) await postHook(); @@ -74,16 +75,16 @@ export class CompactLib { opts: { preHook?: (chain: chain) => Promise; postHook?: () => Promise; - inputToken: Token; + inputToken: TokenContext; account: () => `0x${string}`; - inputAmount: bigint; allocatorId: string; } ) { return async () => { - const { preHook, postHook, inputToken, account, allocatorId, inputAmount } = opts; + const { preHook, postHook, inputToken, account, allocatorId } = opts; + const { token, amount } = inputToken; const publicClients = clients; - const assetId = toId(true, ResetPeriod.OneDay, allocatorId, inputToken.address); + const assetId = toId(true, ResetPeriod.OneDay, allocatorId, token.address); const allocatedTransferStruct: { allocatorData: `0x${string}`; @@ -102,21 +103,21 @@ export class CompactLib { recipients: [ { claimant: BigInt(addressToBytes32(account())), - amount: inputAmount + amount: amount } ] }; - if (preHook) await preHook(inputToken.chain); + if (preHook) await preHook(token.chain); const transactionHash = walletClient.writeContract({ - chain: chainMap[inputToken.chain], + chain: chainMap[token.chain], account: account(), address: COMPACT, abi: COMPACT_ABI, functionName: "allocatedTransfer", args: [allocatedTransferStruct] }); - await publicClients[inputToken.chain].waitForTransactionReceipt({ + await publicClients[token.chain].waitForTransactionReceipt({ hash: await transactionHash }); if (postHook) await postHook(); @@ -129,15 +130,14 @@ export class CompactLib { opts: { preHook?: (chain: chain) => Promise; postHook?: () => Promise; - inputTokens: Token[]; - inputAmounts: bigint[]; + inputTokens: TokenContext[]; account: () => `0x${string}`; } ) { return async () => { - const { preHook, postHook, inputTokens, inputAmounts, account } = opts; + const { preHook, postHook, inputTokens, account } = opts; for (let i = 0; i < inputTokens.length; ++i) { - const inputToken = inputTokens[i]; + const { token: inputToken, amount } = inputTokens[i]; if (preHook) await preHook(inputToken.chain); const publicClient = clients[inputToken.chain]; // Check if we have sufficient allowance already. @@ -147,7 +147,7 @@ export class CompactLib { functionName: "allowance", args: [account(), COMPACT] }); - if (currentAllowance >= inputAmounts[i]) continue; + if (currentAllowance >= amount) continue; const transactionHash = walletClient.writeContract({ chain: chainMap[inputToken.chain], account: account(), diff --git a/src/lib/libraries/intent.ts b/src/lib/libraries/intent.ts index 894226e..487b4b5 100644 --- a/src/lib/libraries/intent.ts +++ b/src/lib/libraries/intent.ts @@ -1,15 +1,29 @@ -import { encodePacked, hashStruct, toHex } from "viem"; -import type { BatchCompact, CompactMandate, MandateOutput, StandardOrder } from "../../types"; +import { encodeAbiParameters, encodePacked, hashStruct, keccak256, parseAbiParameters } from "viem"; +import type { + BatchCompact, + CompactMandate, + Element, + MandateOutput, + MultichainCompact, + MultichainOrder, + MultichainOrderComponent, + NoSignature, + Signature, + StandardOrder +} from "../../types"; import { COMPACT_ABI } from "../abi/compact"; import { chainMap, + clients, COIN_FILLER, COMPACT, + getChainName, getOracle, INPUT_SETTLER_COMPACT_LIFI, INPUT_SETTLER_ESCROW_LIFI, + MULTICHAIN_INPUT_SETTLER_COMPACT, + MULTICHAIN_INPUT_SETTLER_ESCROW, type chain, - type Token, type Verifier, type WC } from "../config"; @@ -17,19 +31,48 @@ import { ResetPeriod, toId } from "../utils/idLib"; import { compact_type_hash, compactTypes } from "../utils/typedMessage"; import { addressToBytes32 } from "../utils/convert"; import { SETTLER_ESCROW_ABI } from "../abi/escrow"; +import type { TokenContext } from "$lib/state.svelte"; +import { MULTICHAIN_SETTLER_ESCROW_ABI } from "$lib/abi/multichain_escrow"; +import { SETTLER_COMPACT_ABI } from "$lib/abi/settlercompact"; +import { toHex } from "$lib/utils/interopableAddresses"; +import { MULTICHAIN_SETTLER_COMPACT_ABI } from "$lib/abi/multichain_compact"; -export type CreateIntentOptions = { - exclusiveFor: string; +type Lock = { + lockTag: `0x${string}`; + token: `0x${string}`; + amount: bigint; +}; + +export type EscrowLock = { + type: "escrow"; +}; + +export type CompactLock = { + type: "compact"; + resetPeriod: ResetPeriod; allocatorId: string; - inputTokens: Token[]; - outputToken: Token; - inputAmounts: bigint[]; - outputAmount: bigint; +}; + +export type CreateIntentOptionsEscrow = { + exclusiveFor: string; + inputTokens: TokenContext[]; + outputTokens: TokenContext[]; verifier: Verifier; account: () => `0x${string}`; - inputSettler: typeof INPUT_SETTLER_COMPACT_LIFI | typeof INPUT_SETTLER_ESCROW_LIFI; + lock: EscrowLock; }; +export type CreateIntentOptionsCompact = { + exclusiveFor: string; + inputTokens: TokenContext[]; + outputTokens: TokenContext[]; + verifier: Verifier; + account: () => `0x${string}`; + lock: CompactLock; +}; + +export type CreateIntentOptions = CreateIntentOptionsEscrow | CreateIntentOptionsCompact; + function findChain(chainId: bigint) { for (const [name, data] of Object.entries(chainMap)) { if (BigInt(data.id) === chainId) { @@ -39,96 +82,255 @@ function findChain(chainId: bigint) { return undefined; } +function selectAllBut(arr: T[], index: number): T[] { + return [...arr.slice(0, index), ...arr.slice(index + 1, arr.length)]; +} + +function encodeOutputs(outputs: MandateOutput[]) { + return encodeAbiParameters( + parseAbiParameters( + "(bytes32 oracle, bytes32 settler, uint256 chainId, bytes32 token, uint256 amount, bytes32 recipient, bytes callbackData, bytes context)[]" + ), + [outputs] + ); +} + +const ONE_MINUTE = 60; +const ONE_HOUR = 60 * ONE_MINUTE; +const ONE_DAY = 24 * ONE_HOUR; + /** * @notice Class representing a Li.Fi Intent. Contains intent abstractions and helpers. */ export class Intent { - private order: StandardOrder; + private lock: EscrowLock | CompactLock; - constructor(opts: CreateIntentOptions) { - const { order } = Intent.create(opts); - this.order = order; + // User facing order options + private user: () => `0x${string}`; + private inputs: TokenContext[]; + private outputs: TokenContext[]; + private verifier: Verifier; + + private exclusiveFor: `0x${string}`; + + private _nonce?: bigint; + + private expiry = ONE_DAY; + private fillDeadline = 2 * ONE_HOUR; + + constructor(opts: CreateIntentOptionsEscrow | CreateIntentOptionsCompact) { + this.lock = opts.lock; + + this.user = opts.account; + this.inputs = opts.inputTokens; + this.outputs = opts.outputTokens; + this.verifier = opts.verifier; + + this.exclusiveFor = opts.exclusiveFor as `0x${string}`; } - static create(opts: CreateIntentOptions) { - const { - exclusiveFor, - allocatorId, - inputTokens, - outputToken, - inputAmounts, - outputAmount, - verifier, - account, - inputSettler - } = opts; + numInputChains() { + const tokenChains = this.inputs.map(({ token }) => token.chain); + return [...new Set(tokenChains)].length; + } + + isMultichain() { + return this.numInputChains() > 1; + } + + isSameChain() { + // Multichain intents cannot be same chain. Normal "Output oracle" will be used. + if (this.isMultichain()) return false; + // Only 1 input chain is used. + const inputChain = this.inputs[0].token.chain; + const outputChains = this.outputs.map((o) => o.token.chain); + const numOutputChains = [...new Set(outputChains)].length; + if (numOutputChains > 1) return false; + // Only 1 output chain is used. + const outputChain = this.outputs[0].token.chain; + return inputChain === outputChain; + } + + nonce() { + if (this._nonce) return this._nonce; + this._nonce = BigInt(Math.floor(Math.random() * 2 ** 32)); + return this._nonce; + } + + inputSettler(multichain: boolean) { + if (this.lock.type === "compact" && multichain === false) return INPUT_SETTLER_COMPACT_LIFI; + if (this.lock.type === "compact" && multichain === true) + return MULTICHAIN_INPUT_SETTLER_COMPACT; + if (this.lock.type === "escrow" && multichain === false) return INPUT_SETTLER_ESCROW_LIFI; + if (this.lock.type === "escrow" && multichain === true) return MULTICHAIN_INPUT_SETTLER_ESCROW; + + throw new Error(`Not supported | multichain: ${multichain}, type: ${this.lock.type}`); + } + + encodeOutputs(currentTime: number) { // Check if exclusiveFor has right formatting: - if (exclusiveFor) { + if (this.exclusiveFor) { // Length should be 42. - const formattedCorrectly = exclusiveFor.length === 42 && exclusiveFor.slice(0, 2) === "0x"; + const formattedCorrectly = + this.exclusiveFor.length === 42 && this.exclusiveFor.slice(0, 2) === "0x"; if (!formattedCorrectly) - throw new Error(`ExclusiveFor not formatted correctly ${exclusiveFor}`); - } - - const inputChain = inputTokens[0].chain; - const inputs: [bigint, bigint][] = []; - for (let i = 0; i < inputTokens.length; ++i) { - // If Compact input, then generate the tokenId otherwise cast into uint256. - const inputTokenId = - inputSettler == INPUT_SETTLER_COMPACT_LIFI - ? toId(true, ResetPeriod.OneDay, allocatorId, inputTokens[i].address) - : BigInt(inputTokens[i].address); - inputs.push([inputTokenId, inputAmounts[i]]); + throw new Error(`ExclusiveFor not formatted correctly ${this.exclusiveFor}`); } - const outputSettler = COIN_FILLER; - const outputOracle = getOracle(verifier, outputToken.chain)!; - const inputOracle = getOracle(verifier, inputChain)!; - // Get the current epoch timestamp: - const currentTime = Math.floor(Date.now() / 1000); + currentTime; const ONE_MINUTE = 60; let context: `0x${string}` = "0x"; - if (exclusiveFor) { - const paddedExclusiveFor: `0x${string}` = `0x${exclusiveFor.replace("0x", "").padStart(64, "0")}`; + if (this.exclusiveFor) { + const paddedExclusiveFor: `0x${string}` = `0x${this.exclusiveFor.replace("0x", "").padStart(64, "0")}`; context = encodePacked( ["bytes1", "bytes32", "uint32"], ["0xe0", paddedExclusiveFor, currentTime + ONE_MINUTE] ); } - // Make Outputs - const output: MandateOutput = { - oracle: addressToBytes32(outputOracle), - settler: addressToBytes32(outputSettler), - chainId: BigInt(chainMap[outputToken.chain].id), - token: addressToBytes32(outputToken.address), - amount: outputAmount, - recipient: addressToBytes32(account()), - callbackData: "0x", - context - }; - const outputs = [output]; + const outputSettler = COIN_FILLER; + const sameChain = this.isSameChain(); + + return this.outputs.map(({ token, amount }) => { + const outputOracle = sameChain + ? addressToBytes32(outputSettler) + : addressToBytes32(getOracle(this.verifier, token.chain)!); + return { + oracle: outputOracle, + settler: addressToBytes32(outputSettler), + chainId: BigInt(chainMap[token.chain].id), + token: addressToBytes32(token.address), + amount: amount, + recipient: addressToBytes32(this.user()), + callbackData: "0x", + context + }; + }) as MandateOutput[]; + } + + singlechain() { + if (this.isMultichain()) + throw new Error(`Not supported as single chain with ${this.numInputChains()} chains`); + + const inputChain = this.inputs[0].token.chain; + + const inputs: [bigint, bigint][] = this.inputs.map(({ token, amount }) => [ + this.lock.type === "compact" + ? toId(true, this.lock.resetPeriod, this.lock.allocatorId, token.address) + : BigInt(token.address), + amount + ]); + + const currentTime = Math.floor(Date.now() / 1000); + + const inputOracle = this.isSameChain() ? COIN_FILLER : getOracle(this.verifier, inputChain)!; - // Make order const order: StandardOrder = { - user: account(), - nonce: BigInt(Math.floor(Math.random() * 2 ** 32)), // Random nonce + user: this.user(), + nonce: this.nonce(), originChainId: BigInt(chainMap[inputChain].id), - fillDeadline: currentTime + ONE_MINUTE * 120, - expires: currentTime + ONE_MINUTE * 120, + fillDeadline: currentTime + this.fillDeadline, + expires: currentTime + this.expiry, inputOracle: inputOracle, inputs: inputs, - outputs: outputs + outputs: this.encodeOutputs(currentTime) }; - return { order }; + return new StandardOrderIntent(this.inputSettler(false), order); + } + + multichain() { + const currentTime = Math.floor(Date.now() / 1000); + + // TODO: Fix before release. The input oracle is not the same on every chain. + const inputOracle = getOracle(this.verifier, this.inputs[0].token.chain)!; + + // Get all unique chains and then get all inputs for each chain. + const inputs: { chainId: bigint; inputs: [bigint, bigint][] }[] = [ + ...new Set(this.inputs.map(({ token }) => token.chain)) + ].map((chain) => { + const chainInputs = this.inputs.filter(({ token }) => token.chain === chain); + + return { + chainId: BigInt(chainMap[chain].id), + inputs: chainInputs.map(({ token, amount }) => [ + this.lock.type === "compact" + ? toId(true, this.lock.resetPeriod, this.lock.allocatorId, token.address) + : BigInt(token.address), + amount + ]) + }; + }); + + const order: MultichainOrder = { + user: this.user(), + nonce: this.nonce(), + fillDeadline: currentTime + this.fillDeadline, + expires: currentTime + this.expiry, + inputOracle: inputOracle, + outputs: this.encodeOutputs(currentTime), + inputs: inputs + }; + + return new MultichainOrderIntent(this.inputSettler(true), order, this.lock); + } + + order() { + if (this.isMultichain()) return this.multichain(); + return this.singlechain(); + } +} + +/// @notice Helper function that allows you to provide an order and it will correctly generate the appropriate order. +export function orderToIntent(options: { + inputSettler: `0x${string}`; + order: StandardOrder; + lock?: { type: string }; +}): StandardOrderIntent; +export function orderToIntent(options: { + inputSettler: `0x${string}`; + order: MultichainOrder; + lock?: { type: string }; +}): MultichainOrderIntent; +export function orderToIntent(options: { + inputSettler: `0x${string}`; + order: StandardOrder | MultichainOrder; + lock?: { type: string }; +}): StandardOrderIntent | MultichainOrderIntent; +export function orderToIntent(options: { + inputSettler: `0x${string}`; + order: StandardOrder | MultichainOrder; + lock?: { type: string }; +}): StandardOrderIntent | MultichainOrderIntent { + const { inputSettler, order, lock } = options; + // Use presence of originChainId to discriminate StandardOrder vs MultichainOrder + if ("originChainId" in order) { + return new StandardOrderIntent(inputSettler, order as StandardOrder); + } + return new MultichainOrderIntent(inputSettler, order as MultichainOrder, lock); +} + +export class StandardOrderIntent { + inputSettler: `0x${string}`; + order: StandardOrder; + + constructor(inputSetter: `0x${string}`, order: StandardOrder) { + this.inputSettler = inputSetter; + this.order = order; } // -- Order Representations -- // + /** + * @notice Returns for logging + */ + asOrder(): StandardOrder { + return this.order; + } + /** * @notice Returns the order as a StandardOrder. * @returns Order as StandardOrder @@ -149,12 +351,8 @@ export class Intent { outputs: order.outputs }; const commitments = order.inputs.map(([tokenId, amount]) => { - const lockTag: `0x${string}` = `0x${toHex(tokenId) - .replace("0x", "") - .slice(0, 12 * 2)}`; - const token: `0x${string}` = `0x${toHex(tokenId) - .replace("0x", "") - .slice(12 * 2, 32 * 2)}`; + const lockTag: `0x${string}` = `0x${toHex(tokenId, 32).slice(0, 12 * 2)}`; + const token: `0x${string}` = `0x${toHex(tokenId, 32).slice(12 * 2, 32 * 2)}`; return { lockTag, token, @@ -165,12 +363,45 @@ export class Intent { arbiter: INPUT_SETTLER_COMPACT_LIFI, sponsor: order.user, nonce: order.nonce, - expires: order.expires, + expires: BigInt(order.expires), commitments, mandate }; } + inputChains(): bigint[] { + return [this.order.originChainId]; + } + + orderId(): `0x${string}` { + return keccak256( + encodePacked( + [ + "uint256", + "address", + "address", + "uint256", + "uint32", + "uint32", + "address", + "bytes32", + "bytes" + ], + [ + this.order.originChainId, + this.inputSettler, + this.order.user, + this.order.nonce, + this.order.expires, + this.order.fillDeadline, + this.order.inputOracle, + keccak256(encodePacked(["uint256[2][]"], [this.order.inputs])), + encodeOutputs(this.order.outputs) + ] + ) + ); + } + // -- Escrow Helpers -- // /** @@ -179,18 +410,21 @@ export class Intent { * @param walletClient Wallet client for sending the call to. * @returns transactionHash for the on-chain call. */ - openEscrow(account: `0x${string}`, walletClient: WC): Promise<`0x${string}`> { + async openEscrow(account: `0x${string}`, walletClient: WC): Promise<[`0x${string}`]> { const chain = findChain(this.order.originChainId); + walletClient.switchChain({ id: Number(this.order.originChainId) }); if (!chain) throw new Error("Chain not found for chainId " + this.order.originChainId.toString()); - return walletClient.writeContract({ - chain, - account, - address: INPUT_SETTLER_ESCROW_LIFI, - abi: SETTLER_ESCROW_ABI, - functionName: "open", - args: [this.order] - }); + return [ + await walletClient.writeContract({ + chain, + account, + address: INPUT_SETTLER_ESCROW_LIFI, + abi: SETTLER_ESCROW_ABI, + functionName: "open", + args: [this.order] + }) + ]; } // -- Compact Helpers -- // @@ -233,4 +467,450 @@ export class Intent { args: [this.order.inputs, [[this.compactClaimHash(), compact_type_hash]]] }); } + + async finalise(options: { + sourceChain: chain; + account: `0x${string}`; + walletClient: WC; + solveParams: { timestamp: number; solver: `0x${string}` }[]; + signatures: { + sponsorSignature: Signature | NoSignature; + allocatorSignature: Signature | NoSignature; + }; + }) { + const { sourceChain, account, walletClient, solveParams, signatures } = options; + const actionChain = chainMap[sourceChain]; + if (actionChain.id !== Number(this.order.originChainId)) + throw new Error( + `Origin chain id and action ID does not match: ${this.order.originChainId}, ${actionChain.id}` + ); + + if (this.inputSettler.toLowerCase() === INPUT_SETTLER_ESCROW_LIFI.toLowerCase()) { + return await walletClient.writeContract({ + chain: actionChain, + account: account, + address: this.inputSettler, + abi: SETTLER_ESCROW_ABI, + functionName: "finalise", + args: [this.order, solveParams, addressToBytes32(account), "0x"] + }); + } else if (this.inputSettler.toLowerCase() === INPUT_SETTLER_COMPACT_LIFI.toLowerCase()) { + // Check whether or not we have a signature. + const { sponsorSignature, allocatorSignature } = signatures; + console.log({ + sponsorSignature, + allocatorSignature + }); + const combinedSignatures = encodeAbiParameters(parseAbiParameters(["bytes", "bytes"]), [ + sponsorSignature.payload ?? "0x", + allocatorSignature.payload + ]); + return await walletClient.writeContract({ + chain: actionChain, + account: account, + address: this.inputSettler, + abi: SETTLER_COMPACT_ABI, + functionName: "finalise", + args: [this.order, combinedSignatures, solveParams, addressToBytes32(account), "0x"] + }); + } else { + throw new Error(`Could not detect settler type ${this.inputSettler}`); + } + } +} + +export class MultichainOrderIntent { + lock?: { type: string } | EscrowLock | CompactLock; + + // Notice that this has to be the same address on every chain. + inputSettler: `0x${string}`; + order: MultichainOrder; + + constructor(inputSetter: `0x${string}`, order: MultichainOrder, lock?: { type: string }) { + this.inputSettler = inputSetter; + this.order = order; + + const isCompact = + this.inputSettler === INPUT_SETTLER_COMPACT_LIFI || + this.inputSettler === MULTICHAIN_INPUT_SETTLER_COMPACT; + + this.lock = lock ?? { type: isCompact ? "compact" : "escrow" }; + } + + selfTest() { + this.asOrder(); + this.inputChains(); + this.asComponents(); + + this.orderId(); + } + + /** + * @notice Returns for logging + */ + asOrder(): MultichainOrder { + return this.order; + } + + inputChains(): bigint[] { + return [...new Set(this.order.inputs.map((i) => i.chainId))]; + } + + orderId(): `0x${string}` { + // We need a random order components. + const components = this.asComponents(); + const computedOrderIds = components.map((c) => + this.lock?.type === "escrow" + ? MultichainOrderIntent.escrowOrderId(this.inputSettler, c.orderComponent, c.chainId) + : MultichainOrderIntent.compactOrderid(this.inputSettler, c.orderComponent, c.chainId) + ); + + const orderId = computedOrderIds[0]; + computedOrderIds.map((v) => { + if (v !== orderId) throw new Error(`Order ids are not equal ${computedOrderIds}`); + }); + if (this.lock?.type === "compact") { + const multichainCompactHash = hashStruct({ + data: this.asMultichainBatchCompact(), + types: compactTypes, + primaryType: "MultichainCompact" + }); + if (multichainCompactHash !== orderId) + throw new Error( + `MultichainCompact does not match orderId, ${multichainCompactHash} ${orderId}` + ); + } + return orderId; + } + + async orderIdCheck() { + const components = this.asComponents(); + const computedOrderId = this.orderId(); + const onChainOrderIds = await Promise.all( + components.map(async (component) => { + const onChainId = await clients[getChainName(component.chainId)].readContract({ + address: this.inputSettler, + abi: MULTICHAIN_SETTLER_COMPACT_ABI, + functionName: "orderIdentifier", + args: [component.orderComponent] + }); + return onChainId; + }) + ); + console.log({ computedOrderId, onChainOrderIds }); + } + + static escrowOrderId( + inputSettler: `0x${string}`, + orderComponent: MultichainOrderComponent, + _: bigint + ) { + return keccak256( + encodePacked( + ["address", "address", "uint256", "uint32", "uint32", "address", "bytes32", "bytes"], + [ + inputSettler, + orderComponent.user, + orderComponent.nonce, + orderComponent.expires, + orderComponent.fillDeadline, + orderComponent.inputOracle, + MultichainOrderIntent.constructInputHash( + orderComponent.chainIdField, + orderComponent.chainIndex, + orderComponent.inputs, + orderComponent.additionalChains + ), + encodeOutputs(orderComponent.outputs) + ] + ) + ); + } + + static compactOrderid( + inputSettler: `0x${string}`, + orderComponent: MultichainOrderComponent, + chainId: bigint + ) { + const MULTICHAIN_COMPACT_TYPEHASH_WITH_WITNESS = keccak256( + encodePacked( + ["string"], + [ + "MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Element[] elements)Element(address arbiter,uint256 chainId,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint32 fillDeadline,address inputOracle,MandateOutput[] outputs)MandateOutput(bytes32 oracle,bytes32 settler,uint256 chainId,bytes32 token,uint256 amount,bytes32 recipient,bytes callbackData,bytes context)" + ] + ) + ); + const { fillDeadline, inputOracle, outputs, inputs } = orderComponent; + const mandate: CompactMandate = { + fillDeadline, + inputOracle, + outputs + }; + const element: Element = { + arbiter: inputSettler, + chainId: chainId, + commitments: MultichainOrderIntent.inputsToLocks(inputs), + mandate + }; + + const elementHash = hashStruct({ + types: compactTypes, + primaryType: "Element", + data: element + }); + + const elementHashes = [ + ...orderComponent.additionalChains.slice(0, Number(orderComponent.chainIndex)), + elementHash, + ...orderComponent.additionalChains.slice(Number(orderComponent.chainIndex)) + ]; + + return keccak256( + encodeAbiParameters( + parseAbiParameters(["bytes32", "address", "uint256", "uint256", "bytes32"]), + [ + MULTICHAIN_COMPACT_TYPEHASH_WITH_WITNESS, + orderComponent.user, + orderComponent.nonce, + BigInt(orderComponent.expires), + keccak256(encodePacked(["bytes32[]"], [elementHashes])) + ] + ) + ); + } + + static hashInputs(chainId: bigint, inputs: [bigint, bigint][]) { + return keccak256(encodePacked(["uint256", "uint256[2][]"], [chainId, inputs])); + } + + static constructInputHash( + inputsChainId: bigint, + chainIndex: bigint, + inputs: [bigint, bigint][], + additionalChains: `0x${string}`[] + ) { + const inputHash = MultichainOrderIntent.hashInputs(inputsChainId, inputs); + const numSegments = additionalChains.length + 1; + if (numSegments <= chainIndex) + throw new Error(`ChainIndexOutOfRange(${chainIndex},${numSegments})`); + const claimStructure: `0x${string}`[] = []; + for (let i = 0; i < numSegments; ++i) { + const additionalChainsIndex = i > chainIndex ? i - 1 : i; + const inputHashElement = + chainIndex == BigInt(i) ? inputHash : additionalChains[additionalChainsIndex]; + claimStructure[i] = inputHashElement; + } + return keccak256(encodePacked(["bytes32[]"], [claimStructure])); + } + + static inputsToLocks(inputs: [bigint, bigint][]): Lock[] { + return inputs.map((input) => { + const bytes32 = toHex(input[0], 32); + return { + lockTag: `0x${bytes32.slice(0, 12 * 2)}`, + token: `0x${bytes32.slice(12 * 2, 32 * 2)}`, + amount: input[1] + }; + }); + } + + secondariesEcsrow(): { chainIdField: bigint; additionalChains: `0x${string}`[] }[] { + const inputsHash: `0x${string}`[] = this.order.inputs.map((input) => + keccak256(encodePacked(["uint256", "uint256[2][]"], [input.chainId, input.inputs])) + ); + return this.order.inputs.map((v, i) => { + return { + chainIdField: v.chainId, + additionalChains: selectAllBut(inputsHash, i) + }; + }); + } + + asCompactElements() { + const { fillDeadline, inputOracle, outputs, inputs } = this.order; + const mandate: CompactMandate = { + fillDeadline, + inputOracle, + outputs + }; + return inputs.map((inputs) => { + const element: Element = { + arbiter: this.inputSettler, + chainId: inputs.chainId, + commitments: MultichainOrderIntent.inputsToLocks(inputs.inputs), + mandate + }; + return element; + }); + } + + secondariesCompact(): { chainIdField: bigint; additionalChains: `0x${string}`[] }[] { + const { inputs } = this.order; + const elements = this.asCompactElements().map((element) => { + const hash = hashStruct({ + types: compactTypes, + primaryType: "Element", + data: element + }); + return hash; + }); + return inputs.map((_, i) => { + return { + chainIdField: inputs[0].chainId, + additionalChains: selectAllBut(elements, i) + }; + }); + } + + asComponents(): { chainId: bigint; orderComponent: MultichainOrderComponent }[] { + const { inputs, user, nonce, expires, fillDeadline, inputOracle, outputs } = this.order; + if (!this.lock) throw new Error(`No lock provided, cannot compute secondaries.`); + const secondaries = + this.lock.type == "escrow" ? this.secondariesEcsrow() : this.secondariesCompact(); + const components: { chainId: bigint; orderComponent: MultichainOrderComponent }[] = []; + for (let i = 0; i < inputs.length; ++i) { + const { chainIdField, additionalChains } = secondaries[i]; + + const orderComponent: MultichainOrderComponent = { + user: user, + nonce: nonce, + chainIdField: chainIdField, + chainIndex: BigInt(i), + expires: expires, + fillDeadline: fillDeadline, + inputOracle: inputOracle, + inputs: inputs[i].inputs, + outputs: outputs, + additionalChains: additionalChains + }; + components.push({ chainId: inputs[i].chainId, orderComponent }); + } + return components; + } + + // -- Compact Helpers -- // + + asMultichainBatchCompact(): MultichainCompact { + const { order } = this; + const mandate: CompactMandate = { + fillDeadline: order.fillDeadline, + inputOracle: order.inputOracle, + outputs: order.outputs + }; + const result = { + sponsor: order.user, + nonce: order.nonce, + expires: BigInt(order.expires), + elements: this.asCompactElements(), + mandate + }; + return result; + } + + compactClaimHash(): `0x${string}` { + const claimHash = hashStruct({ + data: this.asMultichainBatchCompact(), + types: compactTypes, + primaryType: "MultichainCompact" + }); + return claimHash; + } + + signCompact(account: `0x${string}`, walletClient: WC): Promise<`0x${string}`> { + this.selfTest(); + const chainId = this.order.inputs[0].chainId; + return walletClient.signTypedData({ + account, + domain: { + name: "The Compact", + version: "1", + chainId, + verifyingContract: COMPACT + } as const, + types: compactTypes, + primaryType: "MultichainCompact", + message: this.asMultichainBatchCompact() + }); + } + + // This code is depreciated and needs to be updated. + async openEscrow(account: `0x${string}`, walletClient: WC) { + this.selfTest(); + const components = this.asComponents(); + const results: `0x${string}`[] = []; + for (const { chainId, orderComponent } of components) { + const chain = findChain(chainId)!; + walletClient.switchChain({ id: chain.id }); + results.push( + await walletClient.writeContract({ + chain, + account, + address: this.inputSettler, + abi: MULTICHAIN_SETTLER_ESCROW_ABI, + functionName: "open", + args: [orderComponent] + }) + ); + console.log(results); + } + return results; + } + + async finalise(options: { + sourceChain: chain; + account: `0x${string}`; + walletClient: WC; + solveParams: { timestamp: number; solver: `0x${string}` }[]; + signatures: { + sponsorSignature: Signature | NoSignature; + allocatorSignature: Signature | NoSignature; + }; + }) { + this.asMultichainBatchCompact(); + const { sourceChain, account, walletClient, solveParams, signatures } = options; + const actionChain = chainMap[sourceChain]; + if (actionChain.id in this.inputChains().map((v) => Number(v))) + throw new Error( + `Input chains and action ID does not match: ${this.inputChains()}, ${actionChain.id}` + ); + // Get all components for our chain. + const components = this.asComponents().filter((c) => c.chainId === BigInt(actionChain.id)); + + for (const { orderComponent, chainId } of components) { + if (this.inputSettler.toLowerCase() === MULTICHAIN_INPUT_SETTLER_ESCROW.toLowerCase()) { + return await walletClient.writeContract({ + chain: actionChain, + account: account, + address: this.inputSettler, + abi: MULTICHAIN_SETTLER_ESCROW_ABI, + functionName: "finalise", + args: [orderComponent, solveParams, addressToBytes32(account), "0x"] + }); + } else if ( + this.inputSettler.toLowerCase() === MULTICHAIN_INPUT_SETTLER_COMPACT.toLowerCase() + ) { + const { sponsorSignature, allocatorSignature } = signatures; + console.log({ + orderComponent, + sponsorSignature, + allocatorSignature + }); + + const combinedSignatures = encodeAbiParameters(parseAbiParameters(["bytes", "bytes"]), [ + sponsorSignature.payload ?? "0x", + allocatorSignature.payload + ]); + return await walletClient.writeContract({ + chain: actionChain, + account: account, + address: this.inputSettler, + abi: MULTICHAIN_SETTLER_COMPACT_ABI, + functionName: "finalise", + args: [orderComponent, combinedSignatures, solveParams, addressToBytes32(account), "0x"] + }); + } else { + throw new Error(`Could not detect settler type ${this.inputSettler}`); + } + } + } } diff --git a/src/lib/libraries/intentFactory.ts b/src/lib/libraries/intentFactory.ts index f23fa8d..9a9ed71 100644 --- a/src/lib/libraries/intentFactory.ts +++ b/src/lib/libraries/intentFactory.ts @@ -4,15 +4,23 @@ import { clients, INPUT_SETTLER_COMPACT_LIFI, INPUT_SETTLER_ESCROW_LIFI, + MULTICHAIN_INPUT_SETTLER_ESCROW, type Token, type WC } from "$lib/config"; import { maxUint256 } from "viem"; -import type { NoSignature, OrderContainer, Signature, StandardOrder } from "../../types"; +import type { + MultichainOrder, + NoSignature, + OrderContainer, + Signature, + StandardOrder +} from "../../types"; import { ERC20_ABI } from "$lib/abi/erc20"; import { Intent } from "$lib/libraries/intent"; import { OrderServer } from "$lib/libraries/orderServer"; import type { CreateIntentOptions } from "$lib/libraries/intent"; +import { store, type TokenContext } from "$lib/state.svelte"; /** * @notice Factory class for creating and managing intents. Functions called by integrators. @@ -47,8 +55,8 @@ export class IntentFactory { } private saveOrder(options: { - order: StandardOrder; - inputSettler: typeof INPUT_SETTLER_COMPACT_LIFI | typeof INPUT_SETTLER_ESCROW_LIFI; + order: StandardOrder | MultichainOrder; + inputSettler: `0x${string}`; sponsorSignature?: Signature | NoSignature; allocatorSignature?: Signature | NoSignature; }) { @@ -71,26 +79,34 @@ export class IntentFactory { compact(opts: CreateIntentOptions) { return async () => { const { account, inputTokens } = opts; - const inputChain = inputTokens[0].chain; + const inputChain = inputTokens[0].token.chain; if (this.preHook) await this.preHook(inputChain); - const intent = new Intent(opts); + const intent = new Intent(opts).order(); const sponsorSignature = await intent.signCompact(account(), this.walletClient); console.log({ - order: intent.asStandardOrder(), - batchCompact: intent.asBatchCompact(), + order: intent.asOrder(), sponsorSignature }); - const signedOrder = await this.orderServer.submitOrder({ - orderType: "CatalystCompactOrder", - order: intent.asStandardOrder(), - inputSettler: INPUT_SETTLER_COMPACT_LIFI, - sponsorSignature, - allocatorSignature: "0x" + this.saveOrder({ + order: intent.asOrder(), + inputSettler: intent.inputSettler, + sponsorSignature: { + type: "ECDSA", + payload: sponsorSignature + } }); - console.log("signedOrder", signedOrder); + + // const signedOrder = await this.orderServer.submitOrder({ + // orderType: "CatalystCompactOrder", + // order: intent.asStandardOrder(), + // inputSettler: INPUT_SETTLER_COMPACT_LIFI, + // sponsorSignature, + // allocatorSignature: "0x" + // }); + // console.log("signedOrder", signedOrder); if (this.postHook) await this.postHook(); }; @@ -100,13 +116,13 @@ export class IntentFactory { return async () => { const { inputTokens, account } = opts; const publicClients = clients; - const intent = new Intent(opts); + const intent = new Intent(opts).singlechain(); - if (this.preHook) await this.preHook(inputTokens[0].chain); + if (this.preHook) await this.preHook(inputTokens[0].token.chain); let transactionHash = await intent.depositAndRegisterCompact(account(), this.walletClient); - const recepit = await publicClients[inputTokens[0].chain].waitForTransactionReceipt({ + const receipt = await publicClients[inputTokens[0].token.chain].waitForTransactionReceipt({ hash: transactionHash }); @@ -135,26 +151,29 @@ export class IntentFactory { openIntent(opts: CreateIntentOptions) { return async () => { const { inputTokens, account } = opts; - const intent = new Intent(opts); + const intent = new Intent(opts).order(); - const inputChain = inputTokens[0].chain; + const inputChain = inputTokens[0].token.chain; if (this.preHook) await this.preHook(inputChain); // Execute the open. - const transactionHash = await intent.openEscrow(account(), this.walletClient); + const transactionHashes = await intent.openEscrow(account(), this.walletClient); + console.log({ tsh: transactionHashes }); - await clients[inputChain].waitForTransactionReceipt({ - hash: transactionHash - }); + // for (const hash of transactionHashes) { + // await clients[inputChain].waitForTransactionReceipt({ + // hash: await hash + // }); + // } if (this.postHook) await this.postHook(); this.saveOrder({ - order: intent.asStandardOrder(), - inputSettler: INPUT_SETTLER_ESCROW_LIFI + order: intent.asOrder(), + inputSettler: store.inputSettler }); - return transactionHash; + return transactionHashes; }; } } @@ -164,31 +183,32 @@ export function escrowApprove( opts: { preHook?: (chain: chain) => Promise; postHook?: () => Promise; - inputTokens: Token[]; - inputAmounts: bigint[]; + inputTokens: TokenContext[]; account: () => `0x${string}`; } ) { return async () => { - const { preHook, postHook, inputTokens, inputAmounts, account } = opts; + const settler = store.multichain ? MULTICHAIN_INPUT_SETTLER_ESCROW : INPUT_SETTLER_ESCROW_LIFI; + + const { preHook, postHook, inputTokens, account } = opts; for (let i = 0; i < inputTokens.length; ++i) { - const inputToken = inputTokens[i]; - if (preHook) await preHook(inputToken.chain); - const publicClient = clients[inputToken.chain]; + const { token, amount } = inputTokens[i]; + if (preHook) await preHook(token.chain); + const publicClient = clients[token.chain]; const currentAllowance = await publicClient.readContract({ - address: inputToken.address, + address: token.address, abi: ERC20_ABI, functionName: "allowance", - args: [account(), INPUT_SETTLER_ESCROW_LIFI] + args: [account(), settler] }); - if (currentAllowance >= inputAmounts[i]) continue; + if (currentAllowance >= amount) continue; const transactionHash = walletClient.writeContract({ - chain: chainMap[inputToken.chain], + chain: chainMap[token.chain], account: account(), - address: inputToken.address, + address: token.address, abi: ERC20_ABI, functionName: "approve", - args: [INPUT_SETTLER_ESCROW_LIFI, maxUint256] + args: [settler, maxUint256] }); await publicClient.waitForTransactionReceipt({ diff --git a/src/lib/libraries/solver.ts b/src/lib/libraries/solver.ts index b423a0f..0f2b944 100644 --- a/src/lib/libraries/solver.ts +++ b/src/lib/libraries/solver.ts @@ -3,13 +3,20 @@ import { type chain, chainMap, clients, + COIN_FILLER, getChainName, getOracle, INPUT_SETTLER_COMPACT_LIFI, INPUT_SETTLER_ESCROW_LIFI, type WC } from "$lib/config"; -import { encodeAbiParameters, maxUint256, parseAbiParameters } from "viem"; +import { + encodeAbiParameters, + hashStruct, + maxUint256, + parseAbiParameters, + parseEventLogs +} from "viem"; import type { MandateOutput, OrderContainer } from "../../types"; import { addressToBytes32, bytes32ToAddress } from "$lib/utils/convert"; import axios from "axios"; @@ -17,8 +24,9 @@ import { POLYMER_ORACLE_ABI } from "$lib/abi/polymeroracle"; import { SETTLER_COMPACT_ABI } from "$lib/abi/settlercompact"; import { COIN_FILLER_ABI } from "$lib/abi/outputsettler"; import { ERC20_ABI } from "$lib/abi/erc20"; -import { getOrderId } from "$lib/utils/orderLib"; import { SETTLER_ESCROW_ABI } from "$lib/abi/escrow"; +import { orderToIntent } from "./intent"; +import { compactTypes } from "$lib/utils/typedMessage"; /** * @notice Class for solving intents. Functions called by solvers. @@ -43,18 +51,15 @@ export class Solver { outputs } = args; const publicClients = clients; - const orderId = getOrderId({ order, inputSettler }); - //Check that only 1 output exists. - if (outputs.length !== 1) { - throw new Error("Order must have exactly one output"); - } + const orderId = orderToIntent({ order, inputSettler }).orderId(); const outputChain = getChainName(outputs[0].chainId); console.log({ outputChain }); + let value = 0n; for (const output of outputs) { if (output.token === BYTES32_ZERO) { - // The destination asset cannot be ETH. - throw new Error("Output token cannot be ETH"); + value += output.amount; + continue; } if (output.chainId != outputs[0].chainId) { throw new Error("Filling outputs on multiple chains with single fill call not supported"); @@ -91,6 +96,7 @@ export class Solver { chain: chainMap[outputChain], account: account(), address: bytes32ToAddress(outputs[0].settler), + value, abi: COIN_FILLER_ABI, functionName: "fillOrderOutputs", args: [orderId, outputs, order.fillDeadline, addressToBytes32(account())] @@ -98,7 +104,7 @@ export class Solver { await clients[outputChain].waitForTransactionReceipt({ hash: transactionHash }); - // orderInputs.validate[index] = transcationHash; + // orderInputs.validate[index] = transactionHash; if (postHook) await postHook(); return transactionHash; }; @@ -106,7 +112,13 @@ export class Solver { static validate( walletClient: WC, - args: { orderContainer: OrderContainer; fillTransactionHash: string; mainnet: boolean }, + args: { + output: MandateOutput; + orderContainer: OrderContainer; + fillTransactionHash: string; + sourceChain: chain; + mainnet: boolean; + }, opts: { preHook?: (chain: chain) => Promise; postHook?: () => Promise; @@ -116,34 +128,55 @@ export class Solver { return async () => { const { preHook, postHook, account } = opts; const { - orderContainer: { order }, + output, + orderContainer: { order, inputSettler }, fillTransactionHash, + sourceChain, mainnet } = args; - const sourceChain = getChainName(order.originChainId); const outputChain = getChainName(order.outputs[0].chainId); - if (order.outputs.length !== 1) { - throw new Error("Order must have exactly one output"); - } - // The destination asset cannot be ETH. - const output = order.outputs[0]; - if (order.inputOracle === getOracle("polymer", sourceChain)) { - const transactionReceipt = await clients[outputChain].getTransactionReceipt({ - hash: fillTransactionHash as `0x${string}` - }); + // Get the output filled event. + const transactionReceipt = await clients[outputChain].getTransactionReceipt({ + hash: fillTransactionHash as `0x${string}` + }); - const numlogs = transactionReceipt.logs.length; - if (numlogs !== 2) throw Error(`Unexpected Logs count ${numlogs}`); - const fillLog = transactionReceipt.logs[1]; // The first log is transfer, next is fill. + const logs = parseEventLogs({ + abi: COIN_FILLER_ABI, + eventName: "OutputFilled", + logs: transactionReceipt.logs + }); + // We need to search through each log until we find one matching our output. + console.log("logs", logs); + let logIndex = -1; + const expectedOutputHash = hashStruct({ + types: compactTypes, + primaryType: "MandateOutput", + data: output + }); + for (const log of logs) { + const logOutput = log.args.output; + // TODO: Optimise by comparing the dicts. + const logOutputHash = hashStruct({ + types: compactTypes, + primaryType: "MandateOutput", + data: logOutput + }); + if (logOutputHash === expectedOutputHash) { + logIndex = log.logIndex; + break; + } + } + if (logIndex === -1) throw Error(`Could not find matching log`); + if (order.inputOracle === getOracle("polymer", sourceChain)) { let proof: string | undefined; let polymerIndex: number | undefined; for (let i = 0; i < 5; ++i) { const response = await axios.post(`/polymer`, { srcChainId: Number(order.outputs[0].chainId), srcBlockNumber: Number(transactionReceipt.blockNumber), - globalLogIndex: Number(fillLog.logIndex), + globalLogIndex: Number(logIndex), polymerIndex, mainnet: mainnet }); @@ -160,11 +193,11 @@ export class Solver { // Wait while backing off before requesting again. await new Promise((r) => setTimeout(r, i * 2 + 1000)); } - console.log({ proof }); + console.log({ logIndex, proof }); if (proof) { if (preHook) await preHook(sourceChain); - const transcationHash = await walletClient.writeContract({ + const transactionHash = await walletClient.writeContract({ chain: chainMap[sourceChain], account: account(), address: order.inputOracle, @@ -174,33 +207,28 @@ export class Solver { }); const result = await clients[sourceChain].waitForTransactionReceipt({ - hash: transcationHash + hash: transactionHash }); if (postHook) await postHook(); return result; } - } + } else if (order.inputOracle === COIN_FILLER) { + const log = logs.find((log) => log.logIndex === logIndex)!; + const transactionHash = await walletClient.writeContract({ + chain: chainMap[sourceChain], + account: account(), + address: order.inputOracle, + abi: COIN_FILLER_ABI, + functionName: "setAttestation", + args: [log.args.orderId, log.args.solver, log.args.timestamp, log.args.output] + }); - // if (order.inputOracle === getOracle("wormhole", sourceChain)) { - // // TODO: get sequence from event. - // const sequence = 0; - // // Get VAA - // const wormholeChainId = wormholeChainIds[outputChain]; - // const requestUrl = `https://api.testnet.wormholescan.io/v1/signed_vaa/${wormholeChainId}/${output.oracle.replace( - // "0x", - // "" - // )}/${sequence}?network=Testnet`; - // const response = await axios.get(requestUrl); - // console.log(response.data); - // return $walletClient.writeContract({ - // account: connectedAccount.address, - // address: order.inputOracle, - // abi: WROMHOLE_ORACLE_ABI, - // functionName: 'receiveMessage', - // args: [encodedOutput] - // }); - // return; - // } + const result = await clients[sourceChain].waitForTransactionReceipt({ + hash: transactionHash + }); + if (postHook) await postHook(); + return result; + } }; } @@ -208,7 +236,8 @@ export class Solver { walletClient: WC, args: { orderContainer: OrderContainer; - fillTransactionHash: string; + fillTransactionHashes: string[]; + sourceChain: chain; }, opts: { preHook?: (chain: chain) => Promise; @@ -218,67 +247,48 @@ export class Solver { ) { return async () => { const { preHook, postHook, account } = opts; - const { orderContainer, fillTransactionHash } = args; - const { order } = orderContainer; - const outputChain = getChainName(order.outputs[0].chainId); - if (order.outputs.length !== 1) { - throw new Error("Order must have exactly one output"); - } - const transactionReceipt = await clients[outputChain].getTransactionReceipt({ - hash: fillTransactionHash as `0x${string}` - }); - const blockHashOfFill = transactionReceipt.blockHash; - const block = await clients[outputChain].getBlock({ - blockHash: blockHashOfFill + const { orderContainer, fillTransactionHashes, sourceChain } = args; + const { order, inputSettler } = orderContainer; + const intent = orderToIntent({ + inputSettler, + order }); - const fillTimestamp = block.timestamp; - const sourceChain = getChainName(order.originChainId); - if (preHook) await preHook(sourceChain); + const outputChain = getChainName(order.outputs[0].chainId); + const transactionReceipts = await Promise.all( + fillTransactionHashes.map((fth) => + clients[outputChain].getTransactionReceipt({ + hash: fth as `0x${string}` + }) + ) + ); + const blocks = await Promise.all( + transactionReceipts.map((r) => + clients[outputChain].getBlock({ + blockHash: r.blockHash + }) + ) + ); + const fillTimestamps = blocks.map((b) => b.timestamp); - const inputSettler = orderContainer.inputSettler; - console.log({ orderContainer }); - let transactionHash: `0x${string}`; - const actionChain = chainMap[sourceChain]; + if (preHook) await preHook(sourceChain); - const solveParam = { - timestamp: Number(fillTimestamp), - solver: addressToBytes32(account()) - }; + const solveParams = fillTimestamps.map((fillTimestamp) => { + return { + timestamp: Number(fillTimestamp), + solver: addressToBytes32(account()) + }; + }); - if (inputSettler.toLowerCase() === INPUT_SETTLER_ESCROW_LIFI.toLowerCase()) { - transactionHash = await walletClient.writeContract({ - chain: actionChain, - account: account(), - address: inputSettler, - abi: SETTLER_ESCROW_ABI, - functionName: "finalise", - args: [order, [solveParam], addressToBytes32(account()), "0x"] - }); - } else if (inputSettler.toLowerCase() === INPUT_SETTLER_COMPACT_LIFI.toLowerCase()) { - // Check whether or not we have a signature. - const { sponsorSignature, allocatorSignature } = orderContainer; - console.log({ - sponsorSignature, - allocatorSignature - }); - const combinedSignatures = encodeAbiParameters(parseAbiParameters(["bytes", "bytes"]), [ - sponsorSignature.payload ?? "0x", - allocatorSignature.payload - ]); - transactionHash = await walletClient.writeContract({ - chain: actionChain, - account: account(), - address: inputSettler, - abi: SETTLER_COMPACT_ABI, - functionName: "finalise", - args: [order, combinedSignatures, [solveParam], addressToBytes32(account()), "0x"] - }); - } else { - throw new Error(`Could not detect settler type ${orderContainer.inputSettler}`); - } + const transactionHash = await intent.finalise({ + sourceChain, + account: account(), + walletClient, + solveParams, + signatures: orderContainer + }); const result = await clients[sourceChain].waitForTransactionReceipt({ - hash: transactionHash + hash: transactionHash! }); if (postHook) await postHook(); return result; diff --git a/src/lib/libraries/token.ts b/src/lib/libraries/token.ts new file mode 100644 index 0000000..2ae71f7 --- /dev/null +++ b/src/lib/libraries/token.ts @@ -0,0 +1,59 @@ +import { maxUint256 } from "viem"; +import { COMPACT_ABI } from "../abi/compact"; +import { ERC20_ABI } from "../abi/erc20"; +import { ADDRESS_ZERO, clients, COMPACT } from "../config"; +import { ResetPeriod, toId } from "../utils/idLib"; + +export async function getBalance( + user: `0x${string}` | undefined, + asset: `0x${string}`, + client: (typeof clients)[keyof typeof clients] +) { + if (!user) return 0n; + if (asset === ADDRESS_ZERO) { + return client.getBalance({ + address: user, + blockTag: "latest" + }); + } else { + return client.readContract({ + address: asset, + abi: ERC20_ABI, + functionName: "balanceOf", + args: [user] + }); + } +} + +export function getAllowance(contract: `0x${string}`) { + return async ( + user: `0x${string}` | undefined, + asset: `0x${string}`, + client: (typeof clients)[keyof typeof clients] + ) => { + if (!user) return 0n; + if (asset == ADDRESS_ZERO) return maxUint256; + return client.readContract({ + address: asset, + abi: ERC20_ABI, + functionName: "allowance", + args: [user, contract] + }); + }; +} + +export async function getCompactBalance( + user: `0x${string}` | undefined, + asset: `0x${string}`, + client: (typeof clients)[keyof typeof clients], + allocatorId: string +) { + if (!user) return 0n; + const assetId = toId(true, ResetPeriod.OneDay, allocatorId, asset); + return client.readContract({ + address: COMPACT, + abi: COMPACT_ABI, + functionName: "balanceOf", + args: [user, assetId] + }); +} diff --git a/src/lib/screens/FillIntent.svelte b/src/lib/screens/FillIntent.svelte index e14aaf2..c086650 100644 --- a/src/lib/screens/FillIntent.svelte +++ b/src/lib/screens/FillIntent.svelte @@ -9,25 +9,25 @@ type WC } from "$lib/config"; import { bytes32ToAddress } from "$lib/utils/convert"; - import { getOrderId, getOutputHash } from "$lib/utils/orderLib"; + import { getOutputHash } from "$lib/utils/orderLib"; import type { MandateOutput, OrderContainer } from "../../types"; import { Solver } from "$lib/libraries/solver"; import { COIN_FILLER_ABI } from "$lib/abi/outputsettler"; import AwaitButton from "$lib/components/AwaitButton.svelte"; + import store from "$lib/state.svelte"; + import { Intent, orderToIntent } from "$lib/libraries/intent"; + import { compactTypes } from "$lib/utils/typedMessage"; + import { hashStruct } from "viem"; let { scroll, orderContainer, - walletClient, - fillTransactionHash = $bindable(), account, preHook, postHook }: { scroll: (direction: boolean | number) => () => void; orderContainer: OrderContainer; - walletClient: WC; - fillTransactionHash: `0x${string}` | undefined; preHook?: (chain: chain) => Promise; postHook: () => Promise; account: () => `0x${string}`; @@ -49,40 +49,48 @@ functionName: "getFillRecord", args: [orderId, outputHash] }); - console.log({ orderId, output, result, outputHash }); return result; } function sortOutputsByChain(orderContainer: OrderContainer) { const outputs = orderContainer.order.outputs; - const postionMap: { [chainId: string]: number } = {}; + const positionMap: { [chainId: string]: number } = {}; const arrMap: [bigint, MandateOutput[]][] = []; for (const output of outputs) { const chainId = output.chainId; // Check if chainId exists. - let position = postionMap[chainId.toString()]; + let position = positionMap[chainId.toString()]; if (position == undefined) { position = arrMap.length; - postionMap[chainId.toString()] = position; + positionMap[chainId.toString()] = position; arrMap.push([chainId, []]); } arrMap[position][1].push(output); } - console.log(arrMap); return arrMap; } const filledStatusPromises: [bigint, Promise<`0x${string}`>[]][] = $derived( sortOutputsByChain(orderContainer).map(([c, outputs]) => [ c, - outputs.map((output) => isFilled(getOrderId(orderContainer), output, refreshValidation)) + outputs.map((output) => + isFilled(orderToIntent(orderContainer).orderId(), output, refreshValidation) + ) ]) ); - const fillWrapper = (func: ReturnType) => { + const fillWrapper = (outputs: MandateOutput[], func: ReturnType) => { return async () => { const result = await func(); - fillTransactionHash = result; + + for (const output of outputs) { + const outputHash = hashStruct({ + data: output, + types: compactTypes, + primaryType: "MandateOutput" + }); + store.fillTransactions[outputHash] = result; + } }; }; @@ -94,9 +102,9 @@ hash in the input box.

- {#each sortOutputsByChain(orderContainer) as [chainId, outputs], c} + {#each sortOutputsByChain(orderContainer) as chainIdAndOutputs, c}

- {getChainName(chainId)} + {getChainName(chainIdAndOutputs[0])}


@@ -108,11 +116,12 @@ v == BYTES32_ZERO) ? fillWrapper( + chainIdAndOutputs[1], Solver.fill( - walletClient, + store.walletClient, { orderContainer, - outputs + outputs: chainIdAndOutputs[1] }, { preHook, @@ -131,7 +140,7 @@ {/snippet} {/await} - {#each outputs as output, i} + {#each chainIdAndOutputs[1] as output, i} {#await filledStatusPromises[c][1][i]}
@@ -139,7 +148,10 @@
{formatTokenAmount( output.amount, - getCoin({ address: output.token, chain: getChainName(output.chainId) }) + getCoin({ + address: output.token, + chain: getChainName(output.chainId) + }).decimals )}
@@ -160,7 +172,10 @@
{formatTokenAmount( output.amount, - getCoin({ address: output.token, chain: getChainName(output.chainId) }) + getCoin({ + address: output.token, + chain: getChainName(output.chainId) + }).decimals )}
@@ -172,12 +187,22 @@ {/await} {/each}
+ {/each} - -
diff --git a/src/lib/screens/Finalise.svelte b/src/lib/screens/Finalise.svelte index 58d2b04..eb902e1 100644 --- a/src/lib/screens/Finalise.svelte +++ b/src/lib/screens/Finalise.svelte @@ -11,65 +11,74 @@ getCoin, INPUT_SETTLER_COMPACT_LIFI, INPUT_SETTLER_ESCROW_LIFI, + MULTICHAIN_INPUT_SETTLER_COMPACT, + MULTICHAIN_INPUT_SETTLER_ESCROW, type chain, type WC } from "$lib/config"; import { COMPACT_ABI } from "$lib/abi/compact"; import { SETTLER_ESCROW_ABI } from "$lib/abi/escrow"; import { idToToken } from "$lib/utils/convert"; + import store from "$lib/state.svelte"; + import { orderToIntent } from "$lib/libraries/intent"; + import { hashStruct } from "viem"; + import { compactTypes } from "$lib/utils/typedMessage"; let { orderContainer, - walletClient, - fillTransactionHash, account, preHook, postHook }: { orderContainer: OrderContainer; - walletClient: WC; - fillTransactionHash: `0x${string}`; preHook?: (chain: chain) => Promise; postHook?: () => Promise; account: () => `0x${string}`; } = $props(); + let refreshClaimed = $state(0); + const postHookRefreshValidate = async () => { + if (postHook) await postHook(); + refreshClaimed += 1; + }; + // Order status enum const OrderStatus_None = 0; const OrderStatus_Deposited = 1; const OrderStatus_Claimed = 2; const OrderStatus_Refunded = 3; - async function isClaimed( - container: { order: StandardOrder; inputSettler: `0x${string}` }, - _: any - ) { + async function isClaimed(chainId: bigint, container: OrderContainer, _: any) { const { order, inputSettler } = container; - const inputChainClient = getClient(order.originChainId); + const inputChainClient = getClient(chainId); + const intent = orderToIntent(container); + const orderId = intent.orderId(); // Determine the order type. - if (inputSettler == INPUT_SETTLER_ESCROW_LIFI) { + if ( + inputSettler === INPUT_SETTLER_ESCROW_LIFI || + inputSettler === MULTICHAIN_INPUT_SETTLER_ESCROW + ) { // Check order status - const orderId = await inputChainClient.readContract({ - address: inputSettler, - abi: SETTLER_ESCROW_ABI, - functionName: "orderIdentifier", - args: [order] - }); const orderStatus = await inputChainClient.readContract({ address: inputSettler, abi: SETTLER_ESCROW_ABI, functionName: "orderStatus", args: [orderId] }); - return orderStatus == OrderStatus_Claimed || orderStatus == OrderStatus_Refunded; - } else if (inputSettler == INPUT_SETTLER_COMPACT_LIFI) { + return orderStatus === OrderStatus_Claimed || orderStatus === OrderStatus_Refunded; + } else if ( + inputSettler === INPUT_SETTLER_COMPACT_LIFI || + inputSettler === MULTICHAIN_INPUT_SETTLER_COMPACT + ) { // Check claim status + const flattenedInputs = "originChainId" in order ? order.inputs : order.inputs[0].inputs; + const [token, allocator, resetPeriod, scope] = await inputChainClient.readContract({ address: COMPACT, abi: COMPACT_ABI, functionName: "getLockDetails", - args: [order.inputs[0][0]] + args: [flattenedInputs[0][0]] }); // Check if nonce is spent. return await inputChainClient.readContract({ @@ -85,41 +94,85 @@

Finalise Intent

Finalise the order to receive the inputs.

-
-

- {getChainName(orderContainer.order.originChainId)} -

-
-
- {#await isClaimed(orderContainer, "")} - - {:then isClaimed} - {#if isClaimed} + {#each orderToIntent(orderContainer).inputChains() as inputChain} +
+

+ {getChainName(inputChain)} +

+
+
+ {#await isClaimed(inputChain, orderContainer, "")} - {:else} + {:then isClaimed} + {#if isClaimed} + + {:else} + + store.fillTransactions[ + hashStruct({ + data: output, + types: compactTypes, + primaryType: "MandateOutput" + }) + ] as string + ) + }, + { + account, + preHook, + postHook: postHookRefreshValidate + } + )} + > + {#snippet name()} + Claim + {/snippet} + {#snippet awaiting()} + Waiting for transaction... + {/snippet} + + {/if} + {:catch} + store.fillTransactions[ + hashStruct({ + data: output, + types: compactTypes, + primaryType: "MandateOutput" + }) + ] as string + ) }, { account, preHook, - postHook + postHook: postHookRefreshValidate } )} > @@ -130,55 +183,59 @@ Waiting for transaction... {/snippet} - {/if} - {:catch} - - {#snippet name()} - Claim - {/snippet} - {#snippet awaiting()} - Waiting for transaction... - {/snippet} - - {/await} -
- {#each orderContainer.order.inputs as input} -
-
-
-
- {formatTokenAmount( - input[1], - getCoin({ - address: idToToken(input[0]), - chain: getChainName(orderContainer.order.originChainId) - }) - )} + {/await} +
+ {#if "originChainId" in orderContainer.order} + {#each orderContainer.order.inputs as input} +
+
+
+
+ {formatTokenAmount( + input[1], + getCoin({ + address: idToToken(input[0]), + chain: getChainName(orderContainer.order.originChainId) + }).decimals + )} +
+
+ {getCoin({ + address: idToToken(input[0]), + chain: getChainName(orderContainer.order.originChainId) + }).name} +
+
-
- {getCoin({ - address: idToToken(input[0]), - chain: getChainName(orderContainer.order.originChainId) - }).name} +
+ {/each} + {:else} + {#each orderContainer.order.inputs.find((v) => v.chainId === inputChain)?.inputs as input} +
+
+
+
+ {formatTokenAmount( + input[1], + getCoin({ + address: idToToken(input[0]), + chain: getChainName(inputChain) + }).decimals + )} +
+
+ {getCoin({ + address: idToToken(input[0]), + chain: getChainName(inputChain) + }).name} +
+
-
-
- {/each} + {/each} + {/if} +
-
+ {/each}
diff --git a/src/lib/screens/IntentList.svelte b/src/lib/screens/IntentList.svelte index c7fc3b2..6435acb 100644 --- a/src/lib/screens/IntentList.svelte +++ b/src/lib/screens/IntentList.svelte @@ -1,7 +1,7 @@
@@ -33,37 +46,64 @@
OrderId
-
{getOrderId(orderContainer).slice(2, 12)}
+
{orderToIntent(orderContainer).orderId().slice(2, 12)}
User
{orderContainer.order.user.slice(0, 8)}...
Inputs
- {#each orderContainer.order.inputs as input} -
-
-
-
- {formatTokenAmount( - input[1], - getCoin({ + {#if "originChainId" in orderContainer.order} + {#each orderContainer.order.inputs as input} +
+
+
+
+ {formatTokenAmount( + input[1], + getCoin({ + address: idToToken(input[0]), + chain: getChainName(orderContainer.order.originChainId) + }).decimals + )} +
+
+ {getCoin({ address: idToToken(input[0]), chain: getChainName(orderContainer.order.originChainId) - }) - )} + }).name} +
-
- {getCoin({ - address: idToToken(input[0]), - chain: getChainName(orderContainer.order.originChainId) - }).name} +
{getChainName(orderContainer.order.originChainId)}
+
+
+ {/each} + {:else} + {#each flattenInputs(orderContainer.order.inputs) as input} +
+
+
+
+ {formatTokenAmount( + input.input[1], + getCoin({ + address: idToToken(input.input[0]), + chain: getChainName(input.chainId) + }).decimals + )} +
+
+ {getCoin({ + address: idToToken(input.input[0]), + chain: getChainName(input.chainId) + }).name} +
+
{getChainName(input.chainId)}
-
{getChainName(orderContainer.order.originChainId)}
-
- {/each} + {/each} + {/if}
@@ -76,6 +116,7 @@ {formatTokenAmount( output.amount, getCoin({ address: output.token, chain: getChainName(output.chainId) }) + .decimals )}
diff --git a/src/lib/screens/IssueIntent.svelte b/src/lib/screens/IssueIntent.svelte index 911b78f..f162a9f 100644 --- a/src/lib/screens/IssueIntent.svelte +++ b/src/lib/screens/IssueIntent.svelte @@ -3,80 +3,51 @@ import GetQuote from "$lib/components/GetQuote.svelte"; import { INPUT_SETTLER_COMPACT_LIFI, - type WC, - type availableAllocators, - type availableInputSettlers, - type balanceQuery, - type Token, - type Verifier, POLYMER_ALLOCATOR, formatTokenAmount, type chain, - INPUT_SETTLER_ESCROW_LIFI + INPUT_SETTLER_ESCROW_LIFI, + MULTICHAIN_INPUT_SETTLER_COMPACT } from "$lib/config"; import { IntentFactory, escrowApprove } from "$lib/libraries/intentFactory"; import { CompactLib } from "$lib/libraries/compactLib"; - import type { OrderContainer } from "../../types"; + import store from "$lib/state.svelte"; + import InputTokenModal from "../components/InputTokenModal.svelte"; + import OutputTokenModal from "$lib/components/OutputTokenModal.svelte"; + import { ResetPeriod } from "$lib/utils/idLib"; + import type { CreateIntentOptions } from "$lib/libraries/intent"; + + const bigIntSum = (...nums: bigint[]) => nums.reduce((a, b) => a + b, 0n); let { scroll, - showTokenSelector = $bindable(), - exclusiveFor = $bindable(), - mainnet, - inputSettler, - allocatorId, - inputAmounts, - outputAmount, - inputTokens, - outputToken, - verifier, - compactBalances, - balances, - allowances, - walletClient, - orders = $bindable(), preHook, postHook, account }: { scroll: (direction: boolean | number) => () => void; - showTokenSelector: { - active: number; - input: boolean; - index: number; - }; - exclusiveFor: string; - inputSettler: availableInputSettlers; - mainnet: boolean; - allocatorId: availableAllocators; - inputAmounts: bigint[]; - outputAmount: bigint; - inputTokens: Token[]; - outputToken: Token; - compactBalances: balanceQuery; - verifier: Verifier; - balances: balanceQuery; - allowances: balanceQuery; - walletClient: WC; - orders: OrderContainer[]; preHook?: (chain: chain) => Promise; postHook: () => Promise; account: () => `0x${string}`; } = $props(); + let inputTokenSelectorActive = $state(false); + let outputTokenSelectorActive = $state(false); + const opts = $derived({ - exclusiveFor, - allocatorId, - inputTokens, + exclusiveFor: store.exclusiveFor, + inputTokens: store.inputTokens, + outputTokens: store.outputTokens, preHook, postHook, - outputToken, - inputAmounts, - outputAmount, - verifier, - inputSettler, + verifier: store.verifier, + lock: { + type: store.intentType, + allocatorId: store.allocatorId, + resetPeriod: ResetPeriod.OneDay + }, account - }); + } as CreateIntentOptions); const postHookScroll = async () => { await postHook(); @@ -85,107 +56,149 @@ const intentFactory = $derived( new IntentFactory({ - mainnet, - walletClient, + mainnet: store.mainnet, + walletClient: store.walletClient, preHook, postHook: postHookScroll, - ordersPointer: orders + ordersPointer: store.orders }) ); const approveFunction = $derived( - inputSettler === INPUT_SETTLER_COMPACT_LIFI - ? CompactLib.compactApprove(walletClient, opts) - : escrowApprove(walletClient, opts) + store.intentType === "compact" + ? CompactLib.compactApprove(store.walletClient, opts) + : escrowApprove(store.walletClient, opts) ); let allowanceCheck = $state(true); $effect(() => { allowanceCheck = true; - if (!allowances[inputTokens[0].chain]) { + if (!store.allowances[store.inputTokens[0].token.chain]) { allowanceCheck = false; return; } - for (let i = 0; i < inputTokens.length; ++i) { - const token = inputTokens[i]; - const inputAmount = inputAmounts[i]; - allowances[token.chain][token.address].then((a) => { - allowanceCheck = allowanceCheck && a >= inputAmount; + for (let i = 0; i < store.inputTokens.length; ++i) { + const { token, amount } = store.inputTokens[i]; + store.allowances[token.chain][token.address].then((a) => { + allowanceCheck = allowanceCheck && a >= amount; }); } }); let balanceCheckWallet = $state(true); $effect(() => { balanceCheckWallet = true; - if (!balances[inputTokens[0].chain]) { + if (!store.balances[store.inputTokens[0].token.chain]) { balanceCheckWallet = false; return; } - for (let i = 0; i < inputTokens.length; ++i) { - const token = inputTokens[i]; - const inputAmount = inputAmounts[i]; - balances[token.chain][token.address].then((b) => { - balanceCheckWallet = balanceCheckWallet && b >= inputAmount; + for (let i = 0; i < store.inputTokens.length; ++i) { + const { token, amount } = store.inputTokens[i]; + store.balances[token.chain][token.address].then((b) => { + balanceCheckWallet = balanceCheckWallet && b >= amount; }); } }); let balanceCheckCompact = $state(true); $effect(() => { balanceCheckCompact = true; - if (!compactBalances[inputTokens[0].chain]) { + if (!store.compactBalances[store.inputTokens[0].token.chain]) { balanceCheckCompact = false; return; } - for (let i = 0; i < inputTokens.length; ++i) { - const token = inputTokens[i]; - const inputAmount = inputAmounts[i]; - compactBalances[token.chain][token.address].then((b) => { - balanceCheckCompact = balanceCheckCompact && b >= inputAmount; + for (let i = 0; i < store.inputTokens.length; ++i) { + const { token, amount } = store.inputTokens[i]; + store.compactBalances[token.chain][token.address].then((b) => { + balanceCheckCompact = balanceCheckCompact && b >= amount; }); } }); - const allSameChains = $derived(inputTokens.every((v) => inputTokens[0].chain === v.chain)); + const abstractInputs = $derived.by(() => { + const inputs: { + name: string; + amount: bigint; + decimals: number; + }[] = []; + // Get all unique tokens. + const allUniqueNames = [ + ...new Set( + store.inputTokens.map((v) => { + return v.token.name; + }) + ) + ]; + for (let i = 0; i < allUniqueNames.length; ++i) { + const name = allUniqueNames[i]; + inputs[i] = { + name, + amount: bigIntSum( + ...store.inputTokens.map((v, i) => (v.token.name == name ? v.amount : 0n)) + ), + decimals: store.inputTokens.find((v) => v.token.name == name)!.token.decimals + }; + } + return inputs; + }); + + const numInputChains = $derived.by(() => { + const tokenChains = store.inputTokens.map(({ token }) => token.chain); + const uniqueChains = [...new Set(tokenChains)]; + return uniqueChains.length; + }); + + const sameChain = $derived.by(() => { + if (numInputChains > 1) return false; + + // Only 1 input chain is used. + const inputChain = store.inputTokens[0].token.chain; + const outputChains = store.outputTokens.map((o) => o.token.chain); + const numOutputChains = [...new Set(outputChains)].length; + if (numOutputChains > 1) return false; + const outputChain = outputChains[0]; + return inputChain === outputChain; + }); -
+

Intent Issuance

Select assets for your intent along with the verifier for the intent. Then choose your desired style of execution. Your intent will be sent to the LI.FI dev order server.

+ {#if inputTokenSelectorActive} + + {/if} + {#if outputTokenSelectorActive} + + {/if}
- {#each inputTokens as inputToken, i} +

You Pay

+ {#each abstractInputs as input, i} {/each} - + {#if numInputChains > 1} +
Multichain!
+ {/if} + {#if sameChain} +
SameChain!
+ {/if}
@@ -195,51 +208,48 @@
-
- - - + + {/each}
+
+ +
- Verified by - + {#if sameChain} + Verified by + + {:else} + Verified by + + {/if}
Exclusive For @@ -247,29 +257,13 @@ type="text" class="w-20 rounded border border-gray-800 bg-gray-50 px-2 py-1" placeholder="0x..." - bind:value={exclusiveFor} + bind:value={store.exclusiveFor} />
- {#if !allSameChains} - - {:else if inputTokens.length != 1} - - {:else if !allowanceCheck} + {#if !allowanceCheck} {#snippet name()} Set allowance @@ -288,7 +282,7 @@ > Low Balance - {:else if inputSettler === INPUT_SETTLER_ESCROW_LIFI} + {:else if store.intentType === "escrow"} {#snippet name()} Execute Open @@ -297,17 +291,8 @@ Waiting for transaction... {/snippet} - {:else if inputSettler === INPUT_SETTLER_COMPACT_LIFI} - - {#snippet name()} - Execute Deposit and Open - {/snippet} - {#snippet awaiting()} - Waiting for transaction... - {/snippet} - {/if} - {#if inputSettler === INPUT_SETTLER_COMPACT_LIFI && allocatorId !== POLYMER_ALLOCATOR} + {#if store.intentType === "compact" && store.allocatorId !== POLYMER_ALLOCATOR} {#if !balanceCheckCompact}
{/if}
+ {#if numInputChains > 1 && store.intentType !== "compact"} +

+ You'll need to open the order on {numInputChains} chains. Be prepared and do not interrupt the + process. +

+ {/if}
diff --git a/src/lib/screens/ManageDeposit.svelte b/src/lib/screens/ManageDeposit.svelte index 197cec7..48caaab 100644 --- a/src/lib/screens/ManageDeposit.svelte +++ b/src/lib/screens/ManageDeposit.svelte @@ -18,32 +18,15 @@ import AwaitButton from "$lib/components/AwaitButton.svelte"; import { CompactLib } from "$lib/libraries/compactLib"; import { toBigIntWithDecimals } from "$lib/utils/convert"; + import store from "$lib/state.svelte"; let { scroll, - mainnet = $bindable(), - inputSettler = $bindable(), - allocatorId = $bindable(), - inputNumber = $bindable(), - inputToken = $bindable(), - compactBalances, - balances, - allowances, - walletClient, preHook, postHook, account }: { scroll: (direction: boolean | number) => () => void; - mainnet: boolean; - inputSettler: availableInputSettlers; - allocatorId: availableAllocators; - inputNumber: number; - inputToken: Token; - compactBalances: balanceQuery; - balances: balanceQuery; - allowances: balanceQuery; - walletClient: WC; preHook: (chain: chain) => Promise; postHook: () => Promise; account: () => `0x${string}`; @@ -51,18 +34,23 @@ let manageAssetAction: "deposit" | "withdraw" = $state("deposit"); + let inputNumber = $state(1); + let allowance = $state(0n); - const inputAmount = $derived(toBigIntWithDecimals(inputNumber, inputToken.decimals)); + const inputAmount = $derived(toBigIntWithDecimals(inputNumber, token.decimals)); $effect(() => { // Check if allowances contain the chain. - if (!allowances[inputToken.chain]) { + if (!store.allowances[token.chain]) { allowance = 0n; return; } - allowances[inputToken.chain][inputToken.address].then((a) => { + store.allowances[token.chain][token.address].then((a) => { allowance = a; }); }); + + let selectedTokenIndex = $state(0); + const token = $derived(coinList(store.mainnet)[selectedTokenIndex]);
@@ -76,57 +64,57 @@

Network

Input Type

- +
- {#if inputSettler === INPUT_SETTLER_COMPACT_LIFI} + {#if store.intentType === "compact"}

Allocator

@@ -138,19 +126,17 @@ of - - + {#each coinList(store.mainnet) as tkn, i} + {/each}
@@ -159,13 +145,12 @@
{#if manageAssetAction === "withdraw"} {#snippet name()} @@ -177,11 +162,10 @@ {:else if allowance < inputAmount} @@ -194,13 +178,12 @@ {:else} {#snippet name()} diff --git a/src/lib/screens/ReceiveMessage.svelte b/src/lib/screens/ReceiveMessage.svelte index e045d6c..0dd2288 100644 --- a/src/lib/screens/ReceiveMessage.svelte +++ b/src/lib/screens/ReceiveMessage.svelte @@ -1,38 +1,28 @@
@@ -86,78 +97,93 @@ Click on each output and wait until they turn green. Polymer does not support batch validation. Continue to the right.

-
-

- {getChainName(orderContainer.order.originChainId)} -

-
-
- {#each orderContainer.order.outputs as output} - {#await isValidated(getOrderId(orderContainer), orderContainer, output, fillTransactionHash, refreshValidation)} -
-
-
-
- {formatTokenAmount( - output.amount, - getCoin({ address: output.token, chain: getChainName(output.chainId) }) - )} -
-
- {getCoin({ address: output.token, chain: getChainName(output.chainId) }).name} + {#each orderToIntent(orderContainer).inputChains() as inputChain} +
+

+ {getChainName(inputChain)} +

+
+
+ {#each orderContainer.order.outputs as output} + {#await isValidated(orderToIntent(orderContainer).orderId(), inputChain, orderContainer, output, store.fillTransactions[hashStruct( { data: output, types: compactTypes, primaryType: "MandateOutput" } )], refreshValidation)} +
+
+
+
+ {formatTokenAmount( + output.amount, + getCoin({ address: output.token, chain: getChainName(output.chainId) }) + .decimals + )} +
+
+ {getCoin({ address: output.token, chain: getChainName(output.chainId) }).name} +
-
- {:then validated} - console.log("Has already been validated") - : Solver.validate( - walletClient, - { - orderContainer, - fillTransactionHash, - mainnet - }, - { - preHook, - postHook: postHookRefreshValidate, - account - } - )} - > - {#snippet name()} -
-
- {formatTokenAmount( - output.amount, - getCoin({ address: output.token, chain: getChainName(output.chainId) }) - )} -
-
- {getCoin({ address: output.token, chain: getChainName(output.chainId) }).name} + {:then validated} + + {#snippet name()} +
+
+ {formatTokenAmount( + output.amount, + getCoin({ address: output.token, chain: getChainName(output.chainId) }) + .decimals + )} +
+
+ {getCoin({ address: output.token, chain: getChainName(output.chainId) }).name} +
-
- {/snippet} - {#snippet awaiting()} -
-
- {formatTokenAmount( - output.amount, - getCoin({ address: output.token, chain: getChainName(output.chainId) }) - )} -
-
- {getCoin({ address: output.token, chain: getChainName(output.chainId) }).name} + {/snippet} + {#snippet awaiting()} +
+
+ {formatTokenAmount( + output.amount, + getCoin({ address: output.token, chain: getChainName(output.chainId) }) + .decimals + )} +
+
+ {getCoin({ address: output.token, chain: getChainName(output.chainId) }).name} +
-
- {/snippet} - - {/await} - {/each} + {/snippet} + + {/await} + {/each} +
-
+ {/each}
diff --git a/src/lib/screens/TokenModal.svelte b/src/lib/screens/TokenModal.svelte deleted file mode 100644 index 84aa4d2..0000000 --- a/src/lib/screens/TokenModal.svelte +++ /dev/null @@ -1,142 +0,0 @@ - - -{#if showTokenSelector.active > 0} - -
-
- {#if showTokenSelector.index === -1} -

Add Asset

- {:else} -

Select Asset

- {/if} -
- - {#if showTokenSelector.input} - of - - {:else}{/if} -
-
- -
- -
-
-{/if} diff --git a/src/lib/state.svelte.ts b/src/lib/state.svelte.ts index cfb2856..78d6fe9 100644 --- a/src/lib/state.svelte.ts +++ b/src/lib/state.svelte.ts @@ -1,59 +1,160 @@ -import { maxUint256 } from "viem"; -import { COMPACT_ABI } from "./abi/compact"; -import { ERC20_ABI } from "./abi/erc20"; -import { ADDRESS_ZERO, clients, COMPACT } from "./config"; -import { ResetPeriod, toId } from "./utils/idLib"; - -export async function getBalance( - user: `0x${string}` | undefined, - asset: `0x${string}`, - client: (typeof clients)[keyof typeof clients] -) { - if (!user) return 0n; - if (asset === ADDRESS_ZERO) { - return client.getBalance({ - address: user, - blockTag: "latest" - }); - } else { - return client.readContract({ - address: asset, - abi: ERC20_ABI, - functionName: "balanceOf", - args: [user] - }); +import type { WalletState } from "@web3-onboard/core"; +import type { OrderContainer } from "../types"; +import { + ALWAYS_OK_ALLOCATOR, + chainMap, + clients, + coinList, + COMPACT, + INPUT_SETTLER_COMPACT_LIFI, + INPUT_SETTLER_ESCROW_LIFI, + MULTICHAIN_INPUT_SETTLER_COMPACT, + MULTICHAIN_INPUT_SETTLER_ESCROW, + POLYMER_ALLOCATOR, + type availableAllocators, + type availableInputSettlers, + type chain, + type Token, + type Verifier +} from "./config"; +import { getAllowance, getBalance, getCompactBalance } from "./libraries/token"; +import onboard from "./utils/web3-onboard"; +import { createWalletClient, custom } from "viem"; + +export type TokenContext = { + token: Token; + amount: bigint; +}; + +class Store { + mainnet = $state(true); + orders = $state([]); + + // --- Wallet --- // + wallets = onboard.state.select("wallets"); + activeWallet = $state<{ wallet?: WalletState }>({}); + connectedAccount = $derived(this.activeWallet.wallet?.accounts?.[0]); + walletClient = $derived( + this.activeWallet?.wallet?.provider + ? createWalletClient({ + transport: custom(this.activeWallet?.wallet?.provider) + }) + : undefined + )!; + + // --- Token --- // + inputTokens = $state([]); + outputTokens = $state([]); + + // inputTokens = $state([]); + // outputTokens = $state([]); + // inputAmounts = $state([1000000n]); + // outputAmounts = $state([1000000n]); + + fillTransactions = $state<{ [outputId: string]: `0x${string}` }>({}); + + balances = $derived.by(() => { + return this.mapOverCoins(getBalance, this.mainnet, this.updatedDerived); + }); + allowances = $derived.by(() => { + return this.mapOverCoins( + getAllowance( + this.inputSettler === INPUT_SETTLER_COMPACT_LIFI || + this.inputSettler === MULTICHAIN_INPUT_SETTLER_COMPACT + ? COMPACT + : this.inputSettler + ), + this.mainnet, + this.updatedDerived + ); + }); + compactBalances = $derived.by(() => { + return this.mapOverCoins( + ( + user: `0x${string}` | undefined, + asset: `0x${string}`, + client: (typeof clients)[keyof typeof clients] + ) => getCompactBalance(user, asset, client, this.allocatorId), + this.mainnet, + this.updatedDerived + ); + }); + + multichain = $derived([...new Set(this.inputTokens.map((i) => i.token.chain))].length > 1); + + // --- Input Side --- // + // TODO: remove + inputSettler = $derived.by(() => { + if (this.intentType === "escrow" && !this.multichain) return INPUT_SETTLER_ESCROW_LIFI; + if (this.intentType === "escrow" && this.multichain) return MULTICHAIN_INPUT_SETTLER_ESCROW; + + if (this.intentType === "compact" && !this.multichain) return INPUT_SETTLER_COMPACT_LIFI; + if (this.intentType === "compact" && this.multichain) return MULTICHAIN_INPUT_SETTLER_COMPACT; + + return INPUT_SETTLER_ESCROW_LIFI; + }); + intentType = $state<"escrow" | "compact">("escrow"); + allocatorId = $state(ALWAYS_OK_ALLOCATOR); + + // --- Oracle --- // + verifier = $state("polymer"); + + // --- Output Side --- // + exclusiveFor: string = $state(""); + + // --- Misc --- // + updatedDerived = $state(0); + + forceUpdate = () => { + this.updatedDerived += 1; + }; + + async setWalletToCorrectChain(chain: chain) { + try { + return await this.walletClient?.switchChain({ id: chainMap[chain].id }); + } catch (error) { + console.warn( + `Wallet does not support switchChain or failed to switch chain: ${chainMap[chain].id}`, + error + ); + return undefined; + } } -} -export function getAllowance(contract: `0x${string}`) { - return async ( - user: `0x${string}` | undefined, - asset: `0x${string}`, - client: (typeof clients)[keyof typeof clients] - ) => { - if (!user) return 0n; - if (asset == ADDRESS_ZERO) return maxUint256; - return client.readContract({ - address: asset, - abi: ERC20_ABI, - functionName: "allowance", - args: [user, contract] + mapOverCoins( + func: ( + user: `0x${string}` | undefined, + asset: `0x${string}`, + client: (typeof clients)[keyof typeof clients] + ) => T, + isMainnet: boolean, + _: any + ) { + const resolved: Record> = {} as any; + for (const token of coinList(isMainnet)) { + // Check whether we have me the chain before. + if (!resolved[token.chain as chain]) resolved[token.chain] = {}; + resolved[token.chain][token.address] = func( + this.connectedAccount?.address, + token.address, + clients[token.chain] + ); + } + return resolved; + } + + constructor() { + this.inputTokens = [{ token: coinList(this.mainnet)[0], amount: 1000000n }]; + this.outputTokens = [{ token: coinList(this.mainnet)[1], amount: 1000000n }]; + + this.wallets.subscribe((v) => { + this.activeWallet.wallet = v?.[0]; }); - }; + setInterval(() => { + this.updatedDerived += 1; + }, 10000); + } } -export async function getCompactBalance( - user: `0x${string}` | undefined, - asset: `0x${string}`, - client: (typeof clients)[keyof typeof clients], - allocatorId: string -) { - if (!user) return 0n; - const assetId = toId(true, ResetPeriod.OneDay, allocatorId, asset); - return client.readContract({ - address: COMPACT, - abi: COMPACT_ABI, - functionName: "balanceOf", - args: [user, assetId] - }); -} +export const store = new Store(); +export default store; diff --git a/src/lib/utils/idLib.ts b/src/lib/utils/idLib.ts index 1ea5561..3f1010e 100644 --- a/src/lib/utils/idLib.ts +++ b/src/lib/utils/idLib.ts @@ -21,14 +21,14 @@ export enum ResetPeriod { * - Bits 160-251: allocator ID (92 bits) * - Bits 0-159: token address (20 bytes = 160 bits) * - * @param isMultichain Whether the lock is multichain (maps to scope) + * @param inputChains Whether the lock is multichain (maps to scope) * @param resetPeriod Reset period (0-7) * @param allocatorId Allocator ID as string * @param token Token address as hex string * @returns The derived resource lock ID as a BigInt */ export function toId( - isMultichain: boolean, + inputChains: boolean, resetPeriod: number, allocatorId: string, token: string @@ -40,8 +40,8 @@ export function toId( // Validate token is a valid address and normalize it const normalizedToken = getAddress(token); - // Convert isMultichain to scope (inverse relationship) - const scope = isMultichain ? 0n : 1n; + // Convert inputChains to scope (inverse relationship) + const scope = inputChains ? 0n : 1n; // Convert allocatorId from decimal string to BigInt const allocatorBigInt = BigInt(allocatorId); diff --git a/src/lib/utils/interopableAddresses.ts b/src/lib/utils/interopableAddresses.ts index d779b3d..0ff6e4b 100644 --- a/src/lib/utils/interopableAddresses.ts +++ b/src/lib/utils/interopableAddresses.ts @@ -2,20 +2,35 @@ function padEven(s: string, minimal = 2, pad: string = "0") { return s.padStart(((Math.max(s.length + 1, minimal) / 2) | 0) * 2, pad); } -function toHex(num: number | bigint, bytes: number = 1) { - return padEven(num.toString(16), bytes * 2); +export function toHex( + num: number | bigint, + bytes: number = 1, + prefix?: T +): `${T}${string}` { + const p = (prefix ?? "") as T; + return `${p}${padEven(num.toString(16), bytes * 2)}` as `${T}${string}`; } -export const getInteropableAddress = (address: `0x${string}`, chainId: number | bigint) => { +type Version = "0001"; +type ChainType = "0000"; +type ChainReferenceLength = string; +type ChainReference = string; +type Address = string; + +export type InteropableAddress = + `0x${Version}${ChainType}${ChainReferenceLength}${ChainReference}${Address}`; + +export const getInteropableAddress = ( + address: `0x${string}`, + chainId: number | bigint +): InteropableAddress => { const version = "0001"; const chainType = "0000"; const chainReference = padEven(chainId.toString(16)); const chainReferenceLength = toHex(chainReference.length / 2); - const interopableAddress = `0x${version}${chainType}${chainReferenceLength}${chainReference}${toHex( + return `0x${version}${chainType}${chainReferenceLength}${chainReference}${toHex( address.replace("0x", "").length / 2 )}${address.replace("0x", "")}`; - - return interopableAddress; }; diff --git a/src/lib/utils/multichainOrder.ts b/src/lib/utils/multichainOrder.ts deleted file mode 100644 index dadc724..0000000 --- a/src/lib/utils/multichainOrder.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { encodePacked, hashTypedData, keccak256, toHex } from "viem"; -import type { CompactMandate, MultichainOrder, MultichainOrderComponent } from "../../types"; -import { INPUT_SETTLER_ESCROW_LIFI } from "$lib/config"; -import { compactTypes } from "./typedMessage"; - -function selectAllBut(arr: T[], index: number): T[] { - return [...arr.slice(0, index), ...arr.slice(index + 1, arr.length)]; -} - -function secondariesEcsrow( - order: MultichainOrder -): { chainIdField: bigint; additionalChains: `0x${string}`[] }[] { - const inputsHash: `0x${string}`[] = order.inputs.map((input) => - keccak256(encodePacked(["uint256", "uint256[2][]"], [input.chainId, input.inputs])) - ); - return order.inputs.map((v, i) => { - return { - chainIdField: v.chainId, - additionalChains: selectAllBut(inputsHash, i) - }; - }); -} - -type Lock = { - lockTag: `0x${string}`; - token: `0x${string}`; - amount: bigint; -}; - -function inputsToLocks(inputs: [bigint, bigint][]): Lock[] { - return inputs.map((input) => { - const bytes32 = toHex(input[0]).replace("0x", ""); - return { - lockTag: `0x${bytes32.slice(0, 12 * 2)}`, - token: `0x${bytes32.slice(12 * 2, 32 * 2)}`, - amount: input[1] - }; - }); -} - -function secondariesCompact( - order: MultichainOrder, - inputSettler: `0x${string}` -): { chainIdField: bigint; additionalChains: `0x${string}`[] }[] { - const mandate: CompactMandate = { - fillDeadline: order.fillDeadline, - inputOracle: order.inputOracle, - outputs: order.outputs - }; - const elements = order.inputs.map((inputs) => { - const element: { - arbiter: `0x${string}`; - chainId: bigint; - commitments: Lock[]; - mandate: CompactMandate; - } = { - arbiter: inputSettler, - chainId: inputs.chainId, - commitments: inputsToLocks(inputs.inputs), - mandate - }; - return hashTypedData({ - types: compactTypes, - primaryType: "Element", - message: element - }); - }); - return order.inputs.map((_, i) => { - return { - chainIdField: order.inputs[0].chainId, - additionalChains: selectAllBut(elements, i) - }; - }); -} - -function ComponentizeOrder( - order: MultichainOrder, - inputSettler: `0x${string}` -): MultichainOrderComponent[] { - const inputs = order.inputs; - const secondaries = - inputSettler === INPUT_SETTLER_ESCROW_LIFI - ? secondariesEcsrow(order) - : secondariesCompact(order, inputSettler); - const components: MultichainOrderComponent[] = []; - for (let i = 0; i < inputs.length; ++i) { - const { chainIdField, additionalChains } = secondaries[i]; - - const orderComponent: MultichainOrderComponent = { - user: order.user, - nonce: order.nonce, - chainIdField: chainIdField, - chainIndex: BigInt(i), - expires: order.expires, - fillDeadline: order.fillDeadline, - inputOracle: order.inputOracle, - inputs: order.inputs[i].inputs, - outputs: order.outputs, - additionalChains: additionalChains - }; - components.push(orderComponent); - } - return components; -} diff --git a/src/lib/utils/orderLib.ts b/src/lib/utils/orderLib.ts index 84f6d66..17e9ea9 100644 --- a/src/lib/utils/orderLib.ts +++ b/src/lib/utils/orderLib.ts @@ -1,42 +1,7 @@ import { encodeAbiParameters, encodePacked, keccak256, parseAbiParameters } from "viem"; -import type { MandateOutput, StandardOrder } from "../../types"; +import type { MandateOutput, MultichainOrder, StandardOrder } from "../../types"; import { type chain, chainMap, POLYMER_ORACLE, WORMHOLE_ORACLE } from "$lib/config"; -export function getOrderId(orderContainer: { order: StandardOrder; inputSettler: `0x${string}` }) { - const { order, inputSettler } = orderContainer; - return keccak256( - encodePacked( - [ - "uint256", - "address", - "address", - "uint256", - "uint32", - "uint32", - "address", - "bytes32", - "bytes" - ], - [ - order.originChainId, - inputSettler, - order.user, - order.nonce, - order.expires, - order.fillDeadline, - order.inputOracle, - keccak256(encodePacked(["uint256[2][]"], [order.inputs])), - encodeAbiParameters( - parseAbiParameters( - "(bytes32 oracle, bytes32 settler, uint256 chainId, bytes32 token, uint256 amount, bytes32 recipient, bytes callbackData, bytes context)[]" - ), - [order.outputs] - ) - ] - ) - ); -} - export function getOutputHash(output: MandateOutput) { return keccak256( encodePacked( diff --git a/src/lib/utils/typedMessage.ts b/src/lib/utils/typedMessage.ts index 1174984..d1f1ea0 100644 --- a/src/lib/utils/typedMessage.ts +++ b/src/lib/utils/typedMessage.ts @@ -7,55 +7,44 @@ const BatchCompact = [ { name: "expires", type: "uint256" }, { name: "commitments", type: "Lock[]" }, { name: "mandate", type: "Mandate" } -]; +] as const; const MultichainCompact = [ { name: "sponsor", type: "address" }, { name: "nonce", type: "uint256" }, { name: "expires", type: "uint256" }, { name: "elements", type: "Element[]" } -]; +] as const; const Lock = [ { name: "lockTag", type: "bytes12" }, { name: "token", type: "address" }, { name: "amount", type: "uint256" } -]; +] as const; const Element = [ { name: "arbiter", type: "address" }, { name: "chainId", type: "uint256" }, { name: "commitments", type: "Lock[]" }, { name: "mandate", type: "Mandate" } -]; +] as const; const Mandate = [ { name: "fillDeadline", type: "uint32" }, { name: "inputOracle", type: "address" }, { name: "outputs", type: "MandateOutput[]" } -]; +] as const; -const MandateOutput = [ +export const MandateOutput = [ { name: "oracle", type: "bytes32" }, { name: "settler", type: "bytes32" }, { name: "chainId", type: "uint256" }, { name: "token", type: "bytes32" }, { name: "amount", type: "uint256" }, { name: "recipient", type: "bytes32" }, - { name: "call", type: "bytes" }, + { name: "callbackData", type: "bytes" }, { name: "context", type: "bytes" } -]; - -export const StandardOrderAbi = [ - { name: "user", type: "address" }, - { name: "nonce", type: "uint256" }, - { name: "originChainId", type: "uint256" }, - { name: "expires", type: "uint32" }, - { name: "fillDeadline", type: "uint32" }, - { name: "inputOracle", type: "address" }, - { name: "inputs", type: "uint256[2][]" }, - { name: "outputs", type: "tuple[]", components: MandateOutput } -]; +] as const; // The named list of all type definitions export const compactTypes = { @@ -77,3 +66,14 @@ if (compact_type_hash != compact_type_hash_contract) { `Computed typehash ${compact_type_hash} does not match expected ${compact_type_hash_contract}` ); } + +const multichain_compact_type = + "MultichainCompact(address sponsor,uint256 nonce,uint256 expires,Element[] elements)Element(address arbiter,uint256 chainId,Lock[] commitments,Mandate mandate)Lock(bytes12 lockTag,address token,uint256 amount)Mandate(uint32 fillDeadline,address inputOracle,MandateOutput[] outputs)MandateOutput(bytes32 oracle,bytes32 settler,uint256 chainId,bytes32 token,uint256 amount,bytes32 recipient,bytes callbackData,bytes context)" as const; +export const multichain_compact_type_hash = keccak256(toHex(multichain_compact_type)); +const multichain_compact_type_hash_contract = + "0x6bc0624272798c7a3ff97563d8a009ea96cffd8ea74a971b2946ca790fc50319"; +if (multichain_compact_type_hash != multichain_compact_type_hash_contract) { + throw Error( + `Computed multichain typehash ${multichain_compact_type_hash} does not match expected ${multichain_compact_type_hash_contract}` + ); +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 192d71e..17cb27a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,26 +1,10 @@