Skip to content
This repository was archived by the owner on Dec 23, 2025. It is now read-only.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
10 changes: 8 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ import {
SubmitWithdrawRequest,
getSubmitWithdrawRequestHash,
getWithdrawalAddress,
} from "./messages/v2.2/allocator";
WithdrawalAddressParams,
WithdrawalInitiationMessage,
WithdrawalInitiatedMessage,
} from "./messages/v2.2/withdrawal-execution";

export {
// Order
Expand Down Expand Up @@ -126,8 +129,11 @@ export {
encodeAction,
decodeAction,

// Allocator
// Onchain withdrawals
SubmitWithdrawRequest,
getSubmitWithdrawRequestHash,
getWithdrawalAddress,
WithdrawalAddressParams,
WithdrawalInitiationMessage,
WithdrawalInitiatedMessage,
};
58 changes: 0 additions & 58 deletions src/messages/v2.2/allocator.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/messages/v2.2/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export type ExecutionMessage = {
metadata?: ExecutionMessageMetadata[];
};

export type ExecutionMetadata = Omit<
ExecutionMessageMetadata,
"oracleContract" | "oracleChainId"
>;

export const getExecutionMessageId = (message: ExecutionMessage) => {
return hashStruct({
types: {
Expand Down
137 changes: 137 additions & 0 deletions src/messages/v2.2/withdrawal-execution.ts
Original file line number Diff line number Diff line change
@@ -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;
};
};
22 changes: 22 additions & 0 deletions test/withdrawal-execution.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});