diff --git a/package.json b/package.json index 7d9e340..681fefa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@reservoir0x/relay-protocol-sdk", - "version": "0.0.60", + "version": "0.0.61", "description": "Relay protocol SDK", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/index.ts b/src/index.ts index cb17687..5d65690 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,7 +64,10 @@ import { SubmitWithdrawRequest, getSubmitWithdrawRequestHash, getWithdrawalAddress, -} from "./messages/v2.2/allocator"; + WithdrawalAddressParams, + WithdrawalInitiationMessage, + WithdrawalInitiatedMessage, +} from "./messages/v2.2/withdrawal-execution"; export { // Order @@ -126,8 +129,11 @@ export { encodeAction, decodeAction, - // Allocator + // Onchain withdrawals SubmitWithdrawRequest, getSubmitWithdrawRequestHash, getWithdrawalAddress, + WithdrawalAddressParams, + WithdrawalInitiationMessage, + WithdrawalInitiatedMessage, }; diff --git a/src/messages/v2.2/allocator.ts b/src/messages/v2.2/allocator.ts deleted file mode 100644 index 5d0bc40..0000000 --- a/src/messages/v2.2/allocator.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Hex, Address, encodePacked, keccak256 } from "viem"; - -export interface SubmitWithdrawRequest { - chainId: string; // chainId of the destination chain on which the user will withdraw - depository: string; // address of the depository account - currency: string; - amount: string; // Amount to withdraw - spender: string; // address of the account that owns the balance in the Hub contract (can be an alias) - receiver: string; // Address of the account on the destination chain - data: string; // add tional data - nonce: string; // Nonce for replay protection -} - -export const getSubmitWithdrawRequestHash = ( - request: SubmitWithdrawRequest -) => { - // EIP712 type from RelayAllocator - const PAYLOAD_TYPEHASH = keccak256( - "SubmitWithdrawRequest(uint256 chainId,string depository,string currency,uint256 amount,address spender,string receiver,bytes data,bytes32 nonce)" as Hex - ); - - // Create EIP712 digest - const digest = keccak256( - encodePacked( - [ - "bytes32", - "uint256", - "bytes32", - "bytes32", - "uint256", - "address", - "bytes32", - "bytes32", - "bytes32", - ], - [ - PAYLOAD_TYPEHASH, - BigInt(request.chainId), - keccak256(request.depository as Hex), - keccak256(request.currency as Hex), - BigInt(request.amount), - request.spender as Address, - keccak256(request.receiver as Hex), - keccak256(request.data as Hex), - request.nonce as Hex, - ] - ) - ); - - // The withdrawal address is the digest itself (as a hex string) - return digest; -}; - -export function getWithdrawalAddress(request: SubmitWithdrawRequest): string { - const withdrawalHash = getSubmitWithdrawRequestHash(request); - const withdrawalAddress = withdrawalHash.slice(2).slice(-40); - return `0x${withdrawalAddress}` as `0x${string}`; -} diff --git a/src/messages/v2.2/execution.ts b/src/messages/v2.2/execution.ts index 2b3a85a..b923e4e 100644 --- a/src/messages/v2.2/execution.ts +++ b/src/messages/v2.2/execution.ts @@ -31,6 +31,11 @@ export type ExecutionMessage = { metadata?: ExecutionMessageMetadata[]; }; +export type ExecutionMetadata = Omit< + ExecutionMessageMetadata, + "oracleContract" | "oracleChainId" +>; + export const getExecutionMessageId = (message: ExecutionMessage) => { return hashStruct({ types: { diff --git a/src/messages/v2.2/withdrawal-execution.ts b/src/messages/v2.2/withdrawal-execution.ts new file mode 100644 index 0000000..41ca64c --- /dev/null +++ b/src/messages/v2.2/withdrawal-execution.ts @@ -0,0 +1,137 @@ +import { Hex, Address, encodePacked, keccak256 } from "viem"; + +export interface SubmitWithdrawRequest { + chainId: string; // chainId of the destination chain on which the user will withdraw + depository: string; // address of the depository account + currency: string; + amount: string; // Amount to withdraw + spender: string; // address of the account that owns the balance in the Hub contract (can be an alias) + receiver: string; // Address of the account on the destination chain + data: string; // add tional data + nonce: string; // Nonce for replay protection +} + +export const getSubmitWithdrawRequestHash = ( + request: SubmitWithdrawRequest +) => { + // EIP712 type from RelayAllocator + const PAYLOAD_TYPEHASH = keccak256( + "SubmitWithdrawRequest(uint256 chainId,string depository,string currency,uint256 amount,address spender,string receiver,bytes data,bytes32 nonce)" as Hex + ); + + // Create EIP712 digest + const digest = keccak256( + encodePacked( + [ + "bytes32", + "uint256", + "bytes32", + "bytes32", + "uint256", + "address", + "bytes32", + "bytes32", + "bytes32", + ], + [ + PAYLOAD_TYPEHASH, + BigInt(request.chainId), + keccak256(request.depository as Hex), + keccak256(request.currency as Hex), + BigInt(request.amount), + request.spender as Address, + keccak256(request.receiver as Hex), + keccak256(request.data as Hex), + request.nonce as Hex, + ] + ) + ); + + // The withdrawal address is the digest itself (as a hex string) + return digest; +}; + +export type WithdrawalAddressParams = { + depositoryAddress: string; + depositoryChainId: bigint; + currency: string; + owner: string; + recipientAddress: string; + amount: bigint; + withdrawalNonce: string; +}; + +/** + * Compute deterministic withdrawal address + * + * @param depositoryAddress the depository contract holding the funds on origin chain + * @param depositoryChainId the chain id of the depository contract currently holding the funds + * @param currency the id of the currency as expressed on origin chain (string) + * @param recipientAddress the address that will receive the withdrawn funds on destination chain + * @param owner the address that owns the balance before the withdrawal is initiated + * @param amount the balance to withdraw + * @param withdrawalNonce nonce to prevent collisions for similar withdrawals + * @returns withdrawal address (in lower case) + */ +export function getWithdrawalAddress( + withdrawalParams: WithdrawalAddressParams +): string { + // pack and hash data + const nonce = keccak256( + encodePacked(["string"], [withdrawalParams.withdrawalNonce]) + ); + const hash = keccak256( + encodePacked( + [ + "address", + "uint256", + "string", + "address", + "address", + "uint256", + "bytes32", + ], + [ + withdrawalParams.depositoryAddress as `0x${string}`, + withdrawalParams.depositoryChainId, + withdrawalParams.currency, + withdrawalParams.recipientAddress as `0x${string}`, + withdrawalParams.owner as `0x${string}`, + withdrawalParams.amount, + nonce, + ] + ) + ); + + // get 40 bytes for an address + const withdrawalAddress = hash.slice(2).slice(-40).toLowerCase(); + return `0x${withdrawalAddress}` as `0x${string}`; +} + +// for oracle requests, we replace the hub chain id by a slug (e.g. 'base') +// and we pass the amount as a string +export type WithdrawalAddressRequest = Omit< + WithdrawalAddressParams, + "depositoryChainId" | "amount" +> & { + depositoryChainSlug: string; + amount: string; +}; + +// types for oracle routes +export type WithdrawalInitiationMessage = { + data: WithdrawalAddressRequest & { settlementChainId: string }; + result: { + withdrawalAddress: string; + }; +}; + +export type WithdrawalInitiatedMessage = { + data: WithdrawalAddressRequest & { + settlementChainId: string; + }; + result: { + proofOfWithdrawalAddressBalance: string; + withdrawalAddress: string; + }; +}; diff --git a/test/withdrawal-execution.test.ts b/test/withdrawal-execution.test.ts new file mode 100644 index 0000000..93f9fde --- /dev/null +++ b/test/withdrawal-execution.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from "vitest"; +import { getWithdrawalAddress } from "../src/messages/v2.2/withdrawal-execution"; +import { getAddress } from "viem"; + +describe("getWithdrawalAddress", () => { + it("should return a valid withdrawal address", () => { + const params = { + depositoryAddress: "0x1234567890123456789012345678901234567890", + depositoryChainId: 1n, + currency: "10340230", + recipientAddress: "0x9876543210987654321098765432109876543210", + owner: "0x9876543210987654321098765432109876543210", + amount: 1000n, + withdrawalNonce: "haha", + }; + + const address = getWithdrawalAddress(params); + expect(address).toMatch(/^0x[0-9a-f]{40}$/i); + expect(address).toBeTruthy(); + expect(getAddress(address).toLowerCase()).toMatch(address); + }); +});