sdk/js: Added transfer with payload from Sui

This commit is contained in:
Kevin Peters 2023-06-30 21:16:17 +00:00 committed by kev1n-peters
parent 1d2e26c081
commit 93122bca88
3 changed files with 276 additions and 39 deletions

View File

@ -446,3 +446,57 @@ export const padSuiType = (type: string): string => {
*/
export const trimSuiType = (type: string): string =>
type.replace(/(0x)(0*)/g, "0x");
/**
* Create a new EmitterCap object owned by owner.
* @returns The created EmitterCap object ID
*/
export const newEmitterCap = (
coreBridgePackageId: string,
coreBridgeStateObjectId: string,
owner: string
): TransactionBlock => {
const tx = new TransactionBlock();
const [emitterCap] = tx.moveCall({
target: `${coreBridgePackageId}::emitter::new`,
arguments: [tx.object(coreBridgeStateObjectId)],
});
tx.transferObjects([emitterCap], tx.pure(owner));
return tx;
};
export const getOldestEmitterCapObjectId = async (
provider: JsonRpcProvider,
coreBridgePackageId: string,
owner: string
): Promise<string | null> => {
let oldestVersion: string | null = null;
let oldestObjectId: string | null = null;
let response: PaginatedObjectsResponse | null = null;
let nextCursor;
do {
response = await provider.getOwnedObjects({
owner,
filter: {
StructType: `${coreBridgePackageId}::emitter::EmitterCap`,
},
options: {
showContent: true,
},
cursor: nextCursor,
});
if (!response || !response.data) {
throw new SuiRpcValidationError(response);
}
for (const objectResponse of response.data) {
if (!objectResponse.data) continue;
const { version, objectId } = objectResponse.data;
if (oldestVersion === null || version < oldestVersion) {
oldestVersion = version;
oldestObjectId = objectId;
}
}
nextCursor = response.hasNextPage ? response.nextCursor : undefined;
} while (nextCursor);
return oldestObjectId;
};

View File

@ -12,6 +12,7 @@ import {
Ed25519Keypair,
JsonRpcProvider,
RawSigner,
SUI_TYPE_ARG,
fromB64,
getMoveObjectType,
getPublishedObjectChanges,
@ -22,6 +23,7 @@ import {
approveEth,
attestFromEth,
attestFromSui,
createWrappedOnEth,
createWrappedOnSui,
createWrappedOnSuiPrepare,
getEmitterAddressEth,
@ -33,6 +35,9 @@ import {
getSignedVAAWithRetry,
parseAttestMetaVaa,
parseSequenceFromLogEth,
parseTokenTransferPayload,
parseTokenTransferVaa,
parseVaa,
redeemOnEth,
redeemOnSui,
transferFromEth,
@ -47,12 +52,14 @@ import {
getInnerType,
getPackageId,
getWrappedCoinType,
newEmitterCap,
} from "../../sui";
import {
CHAIN_ID_ETH,
CHAIN_ID_SUI,
CONTRACTS,
hexToUint8Array,
parseTransferPayload,
tryNativeToHexString,
tryNativeToUint8Array,
} from "../../utils";
@ -635,4 +642,119 @@ describe("Sui SDK tests", () => {
// )
// ).toBe(true);
});
test("Transfer Sui token with payload to Ethereum", async () => {
// Attest on Sui
const suiAttestTxPayload = await attestFromSui(
suiProvider,
SUI_CORE_BRIDGE_STATE_OBJECT_ID,
SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
SUI_TYPE_ARG,
BigInt(0),
suiCoreBridgePackageId,
suiTokenBridgePackageId
);
let result = await executeTransactionBlock(suiSigner, suiAttestTxPayload);
result.effects?.status.status === "failure" &&
console.log(JSON.stringify(result.effects, null, 2));
expect(result.effects?.status.status).toBe("success");
const { sequence: attestSequence, emitterAddress: attestEmitterAddress } =
getEmitterAddressAndSequenceFromResponseSui(
suiCoreBridgePackageId,
result
);
expect(attestSequence).toBeTruthy();
expect(attestEmitterAddress).toBeTruthy();
const { vaaBytes: attestVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_SUI,
attestEmitterAddress,
attestSequence,
{
transport: NodeHttpTransport(),
},
1000,
30
);
expect(attestVAA).toBeTruthy();
// Create wrapped on Ethereum
try {
await createWrappedOnEth(ETH_TOKEN_BRIDGE_ADDRESS, ethSigner, attestVAA);
} catch (e) {
// this could fail because the token is already attested (in an unclean env)
}
const { tokenAddress } = parseAttestMetaVaa(attestVAA);
expect(
await getOriginalAssetSui(
suiProvider,
SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
SUI_TYPE_ARG
)
).toMatchObject({
isWrapped: false,
chainId: CHAIN_ID_SUI,
assetAddress: new Uint8Array(tokenAddress),
});
const coins = await suiProvider.getCoins({
owner: suiAddress,
});
expect(coins.data.length).toBeGreaterThan(0);
const payload = Buffer.from("All your base are belong to us");
const transferAmount = parseUnits("1", 8).toBigInt();
// Transfer to Ethereum with payload
const suiTransferTxPayload = await transferFromSui(
suiProvider,
SUI_CORE_BRIDGE_STATE_OBJECT_ID,
SUI_TOKEN_BRIDGE_STATE_OBJECT_ID,
coins.data,
SUI_TYPE_ARG,
transferAmount,
CHAIN_ID_ETH,
tryNativeToUint8Array(ethSigner.address, CHAIN_ID_ETH),
BigInt(0),
BigInt(0),
payload,
undefined,
undefined,
suiAddress
);
result = await executeTransactionBlock(suiSigner, suiTransferTxPayload);
result.effects?.status.status === "failure" &&
console.log(JSON.stringify(result.effects, null, 2));
expect(result.effects?.status.status).toBe("success");
const { sequence, emitterAddress } =
getEmitterAddressAndSequenceFromResponseSui(
suiCoreBridgePackageId,
result
);
expect(sequence).toBeTruthy();
expect(emitterAddress).toBeTruthy();
// Fetch the transfer VAA
const { vaaBytes: transferVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_SUI,
emitterAddress,
sequence!,
{
transport: NodeHttpTransport(),
},
1000,
30
);
const { tokenTransferPayload } = parseTokenTransferVaa(transferVAA);
expect(tokenTransferPayload.toString()).toBe(payload.toString());
// Redeem on Ethereum
await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, ethSigner, transferVAA);
expect(
await getIsTransferCompletedEth(
ETH_TOKEN_BRIDGE_ADDRESS,
ethProvider,
transferVAA
)
).toBe(true);
});
});

View File

@ -63,7 +63,7 @@ import {
createTransferWrappedInstruction,
createTransferWrappedWithPayloadInstruction,
} from "../solana/tokenBridge";
import { getPackageId, isSameType } from "../sui";
import { getOldestEmitterCapObjectId, getPackageId, isSameType } from "../sui";
import { SuiCoinObject } from "../sui/types";
import { isNativeDenom } from "../terra";
import {
@ -922,6 +922,9 @@ export function transferFromAptos(
);
}
/**
* Transfer an asset from Sui to another chain.
*/
export async function transferFromSui(
provider: JsonRpcProvider,
coreBridgeStateObjectId: string,
@ -935,12 +938,9 @@ export async function transferFromSui(
relayerFee: bigint = BigInt(0),
payload: Uint8Array | null = null,
coreBridgePackageId?: string,
tokenBridgePackageId?: string
) {
if (payload !== null) {
throw new Error("Sui transfer with payload not implemented");
}
tokenBridgePackageId?: string,
senderAddress?: string
): Promise<TransactionBlock> {
const [primaryCoin, ...mergeCoins] = coins.filter((coin) =>
isSameType(coin.coinType, coinType)
);
@ -980,36 +980,97 @@ export async function transferFromSui(
arguments: [tx.object(tokenBridgeStateObjectId)],
typeArguments: [coinType],
});
const [transferTicket, dust] = tx.moveCall({
target: `${tokenBridgePackageId}::transfer_tokens::prepare_transfer`,
arguments: [
assetInfo,
transferCoin,
tx.pure(coalesceChainId(recipientChain)),
tx.pure([...recipient]),
tx.pure(relayerFee),
tx.pure(createNonce().readUInt32LE()),
],
typeArguments: [coinType],
});
tx.moveCall({
target: `${tokenBridgePackageId}::coin_utils::return_nonzero`,
arguments: [dust],
typeArguments: [coinType],
});
const [messageTicket] = tx.moveCall({
target: `${tokenBridgePackageId}::transfer_tokens::transfer_tokens`,
arguments: [tx.object(tokenBridgeStateObjectId), transferTicket],
typeArguments: [coinType],
});
tx.moveCall({
target: `${coreBridgePackageId}::publish_message::publish_message`,
arguments: [
tx.object(coreBridgeStateObjectId),
feeCoin,
messageTicket,
tx.object(SUI_CLOCK_OBJECT_ID),
],
});
return tx;
if (payload === null) {
const [transferTicket, dust] = tx.moveCall({
target: `${tokenBridgePackageId}::transfer_tokens::prepare_transfer`,
arguments: [
assetInfo,
transferCoin,
tx.pure(coalesceChainId(recipientChain)),
tx.pure([...recipient]),
tx.pure(relayerFee),
tx.pure(createNonce().readUInt32LE()),
],
typeArguments: [coinType],
});
tx.moveCall({
target: `${tokenBridgePackageId}::coin_utils::return_nonzero`,
arguments: [dust],
typeArguments: [coinType],
});
const [messageTicket] = tx.moveCall({
target: `${tokenBridgePackageId}::transfer_tokens::transfer_tokens`,
arguments: [tx.object(tokenBridgeStateObjectId), transferTicket],
typeArguments: [coinType],
});
tx.moveCall({
target: `${coreBridgePackageId}::publish_message::publish_message`,
arguments: [
tx.object(coreBridgeStateObjectId),
feeCoin,
messageTicket,
tx.object(SUI_CLOCK_OBJECT_ID),
],
});
return tx;
} else {
if (!senderAddress) {
throw new Error("senderAddress is required for transfer with payload");
}
// Get or create a new `EmitterCap`
let isNewEmitterCap = false;
const emitterCap = await (async () => {
const objectId = await getOldestEmitterCapObjectId(
provider,
coreBridgePackageId,
senderAddress
);
if (objectId !== null) {
return tx.object(objectId);
} else {
const [emitterCap] = tx.moveCall({
target: `${coreBridgePackageId}::emitter::new`,
arguments: [tx.object(coreBridgeStateObjectId)],
});
isNewEmitterCap = true;
return emitterCap;
}
})();
const [transferTicket, dust] = tx.moveCall({
target: `${tokenBridgePackageId}::transfer_tokens_with_payload::prepare_transfer`,
arguments: [
emitterCap,
assetInfo,
transferCoin,
tx.pure(coalesceChainId(recipientChain)),
tx.pure([...recipient]),
tx.pure([...payload]),
tx.pure(createNonce().readUInt32LE()),
],
typeArguments: [coinType],
});
tx.moveCall({
target: `${tokenBridgePackageId}::coin_utils::return_nonzero`,
arguments: [dust],
typeArguments: [coinType],
});
const [messageTicket] = tx.moveCall({
target: `${tokenBridgePackageId}::transfer_tokens_with_payload::transfer_tokens_with_payload`,
arguments: [tx.object(tokenBridgeStateObjectId), transferTicket],
typeArguments: [coinType],
});
tx.moveCall({
target: `${coreBridgePackageId}::publish_message::publish_message`,
arguments: [
tx.object(coreBridgeStateObjectId),
feeCoin,
messageTicket,
tx.object(SUI_CLOCK_OBJECT_ID),
],
});
if (isNewEmitterCap) {
tx.transferObjects([emitterCap], tx.pure(senderAddress));
}
return tx;
}
}