Skip to content

Commit

Permalink
feat: export approve and execute function (#186)
Browse files Browse the repository at this point in the history
  • Loading branch information
npty authored Oct 18, 2024
1 parent 86cde3b commit 9d36662
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-masks-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@axelar-network/axelar-cgp-sui': patch
---

feat: export approve and execute functions
14 changes: 12 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@changesets/cli": "^2.27.6",
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
"@types/node": "^20.14.11",
"@types/secp256k1": "^4.0.6",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1",
"chai": "^4.3.7",
Expand Down
201 changes: 201 additions & 0 deletions src/execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { fromHEX } from '@mysten/bcs';
import { bcs } from '@mysten/sui/bcs';
import { SuiClient, SuiTransactionBlockResponse, SuiTransactionBlockResponseOptions } from '@mysten/sui/client';
import { Keypair } from '@mysten/sui/cryptography';
import { Transaction as SuiTransaction } from '@mysten/sui/transactions';
import { arrayify, hexlify, keccak256 } from 'ethers/lib/utils';
import { bcsStructs } from './bcs';
import { TxBuilder } from './tx-builder';
import {
ApprovedMessage,
DiscoveryInfo,
GatewayApprovalInfo,
GatewayInfo,
GatewayMessageType,
MessageInfo,
MoveCall,
MoveCallArgument,
MoveCallType,
RawMoveCall,
} from './types';
import { hashMessage, signMessage } from './utils';

const {
gateway: { WeightedSigners, MessageToSign, Proof, Message, Transaction },
} = bcsStructs;

export async function approve(
client: SuiClient,
keypair: Keypair,
gatewayApprovalInfo: GatewayApprovalInfo,
messageInfo: MessageInfo,
options: SuiTransactionBlockResponseOptions,
) {
const { packageId, gateway, signers, signerKeys, domainSeparator } = gatewayApprovalInfo;

const messageData = bcs.vector(Message).serialize([messageInfo]).toBytes();
const hashed = hashMessage(messageData, GatewayMessageType.ApproveMessages);

const message = MessageToSign.serialize({
domain_separator: fromHEX(domainSeparator),
signers_hash: keccak256(WeightedSigners.serialize(signers).toBytes()),
data_hash: hashed,
}).toBytes();

let minSigners = 0;
let totalWeight = 0;

for (let i = 0; i < signers.signers.length; i++) {
totalWeight += signers.signers[i].weight;

if (totalWeight >= signers.threshold) {
minSigners = i + 1;
break;
}
}

const signatures = signMessage(signerKeys.slice(0, minSigners), message);
const encodedProof = Proof.serialize({
signers,
signatures,
}).toBytes();

const txBuilder = new TxBuilder(client);

await txBuilder.moveCall({
target: `${packageId}::gateway::approve_messages`,
arguments: [gateway, hexlify(messageData), hexlify(encodedProof)],
});

await txBuilder.signAndExecute(keypair, options);
}

export async function execute(
client: SuiClient,
keypair: Keypair,
discoveryInfo: DiscoveryInfo,
gatewayInfo: GatewayInfo,
messageInfo: MessageInfo,
options: SuiTransactionBlockResponseOptions,
): Promise<SuiTransactionBlockResponse> {
let moveCalls = [createInitialMoveCall(discoveryInfo, messageInfo.destination_id)];
let isFinal = false;

while (!isFinal) {
const builder = new TxBuilder(client);

doMoveCalls(builder.tx, moveCalls, messageInfo.payload);

const nextTx = await inspectTransaction(builder, keypair);

isFinal = nextTx.is_final;
moveCalls = nextTx.move_calls;
}

const txBuilder = new TxBuilder(client);

const ApprovedMessage = await createApprovedMessageCall(txBuilder, gatewayInfo, messageInfo);

doMoveCalls(txBuilder.tx, moveCalls, messageInfo.payload, ApprovedMessage);

return txBuilder.signAndExecute(keypair, options);
}

export async function approveAndExecute(
client: SuiClient,
keypair: Keypair,
gatewayApprovalInfo: GatewayApprovalInfo,
discoveryInfo: DiscoveryInfo,
messageInfo: MessageInfo,
options: SuiTransactionBlockResponseOptions = {
showEvents: true,
},
): Promise<SuiTransactionBlockResponse> {
await approve(client, keypair, gatewayApprovalInfo, messageInfo, options);
return execute(client, keypair, discoveryInfo, gatewayApprovalInfo, messageInfo, options);
}

function createInitialMoveCall(discoveryInfo: DiscoveryInfo, destinationId: string): RawMoveCall {
const { packageId, discovery } = discoveryInfo;
const discoveryArg = [MoveCallType.Object, ...arrayify(discovery)];
const targetIdArg = [MoveCallType.Pure, ...arrayify(destinationId)];

return {
function: {
package_id: packageId,
module_name: 'discovery',
name: 'get_transaction',
},
arguments: [discoveryArg, targetIdArg],
type_arguments: [],
};
}

async function inspectTransaction(builder: TxBuilder, keypair: Keypair) {
const resp = await builder.devInspect(keypair.toSuiAddress());

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const txData: any = resp.results?.[0]?.returnValues?.[0]?.[0];

return Transaction.parse(new Uint8Array(txData));
}

function createApprovedMessageCall(builder: TxBuilder, gatewayInfo: GatewayInfo, messageInfo: MessageInfo) {
return builder.moveCall({
target: `${gatewayInfo.packageId}::gateway::take_approved_message`,
arguments: [
gatewayInfo.gateway,
messageInfo.source_chain,
messageInfo.message_id,
messageInfo.source_address,
messageInfo.destination_id,
messageInfo.payload,
],
});
}

/* eslint-disable @typescript-eslint/no-explicit-any */
function doMoveCalls(tx: SuiTransaction, moveCalls: RawMoveCall[], payload: string, ApprovedMessage?: ApprovedMessage) {
const txResults: any[][] = [];

for (const call of moveCalls) {
const moveCall = buildMoveCall(tx, call, payload, txResults, ApprovedMessage);
const txResult = tx.moveCall(moveCall);
txResults.push(Array.isArray(txResult) ? txResult : [txResult]);
}
}

/* eslint-disable @typescript-eslint/no-explicit-any */
function buildMoveCall(
tx: SuiTransaction,
moveCallInfo: RawMoveCall,
payload: string,
previousReturns: any[][],
ApprovedMessage?: ApprovedMessage,
): MoveCall {
const decodeArgs = (args: any[]): MoveCallArgument[] =>
args.map(([argType, ...arg]) => {
switch (argType) {
case MoveCallType.Object:
return tx.object(hexlify(arg));
case MoveCallType.Pure:
return tx.pure(arrayify(arg));
case MoveCallType.ApproveMessage:
return ApprovedMessage;
case MoveCallType.Payload:
return tx.pure(bcs.vector(bcs.U8).serialize(arrayify(payload)));
case MoveCallType.HotPotato:
return previousReturns[arg[1]][arg[2]];
default:
throw new Error(`Invalid argument prefix: ${argType}`);
}
});

const { package_id: packageId, module_name: moduleName, name } = moveCallInfo.function;

return {
target: `${packageId}::${moduleName}::${name}`,
arguments: decodeArgs(moveCallInfo.arguments),
typeArguments: moveCallInfo.type_arguments,
};
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './bcs';
export * from './tx-builder';
export * from './utils';
export * from './types';
export * from './execute';
76 changes: 74 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const { bcs } = require('@mysten/sui/bcs');
const { fromHEX, toHEX } = require('@mysten/bcs');
import { fromHEX, toHEX } from '@mysten/bcs';
import type { SerializedBcs } from '@mysten/bcs';
import { bcs } from '@mysten/sui/bcs';
import type { TransactionArgument } from '@mysten/sui/transactions';

export const SUI_PACKAGE_ID = '0x2';
export const STD_PACKAGE_ID = '0x1';
Expand All @@ -23,6 +25,11 @@ export enum ITSMessageType {
InterchainTokenDeployment = 1,
}

export enum GatewayMessageType {
ApproveMessages = 0,
RotateSigners = 1,
}

export interface DependencyNode extends Dependency {
dependencies: string[];
}
Expand All @@ -31,3 +38,68 @@ export const UID = bcs.fixedArray(32, bcs.u8()).transform({
input: (id: string) => fromHEX(id),
output: (id: number[]) => toHEX(Uint8Array.from(id)),
});

export type Signer = {
pub_key: Uint8Array;
weight: number;
};

export type MessageInfo = {
source_chain: string;
message_id: string;
source_address: string;
destination_id: string;
payload_hash: string;
payload: string;
};

export type GatewayInfo = {
packageId: string;
gateway: string;
};

export type GatewayApprovalInfo = GatewayInfo & {
signers: {
signers: Signer[];
threshold: number;
nonce: string;
};
signerKeys: string[];
domainSeparator: string;
};

export type DiscoveryInfo = {
packageId: string;
discovery: string;
};

export type RawMoveCall = {
function: {
package_id: string;
module_name: string;
name: string;
};
arguments: any[]; // eslint-disable-line @typescript-eslint/no-explicit-any
type_arguments: string[];
};

export type MoveCallArgument = TransactionArgument | SerializedBcs<any>; // eslint-disable-line @typescript-eslint/no-explicit-any

export type MoveCall = {
arguments?: MoveCallArgument[];
typeArguments?: string[];
target: string;
};

export type ApprovedMessage = {
$kind: string;
Result: number;
};

export enum MoveCallType {
Object = 0,
Pure = 1,
ApproveMessage = 2,
Payload = 3,
HotPotato = 4,
}
21 changes: 21 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import fs from 'fs';
import path from 'path';
import { getFullnodeUrl } from '@mysten/sui/client';
import { getFaucetHost, requestSuiFromFaucetV0 } from '@mysten/sui/faucet';
import { arrayify, keccak256 } from 'ethers/lib/utils';
import secp256k1 from 'secp256k1';
import toml from 'smol-toml';
import { Dependency, DependencyNode, InterchainTokenOptions } from './types';

Expand Down Expand Up @@ -214,3 +216,22 @@ export function parseEnv(arg: string) {
return JSON.parse(arg);
}
}

export function hashMessage(data: Uint8Array, commandType: number) {
const toHash = new Uint8Array(data.length + 1);
toHash[0] = commandType;
toHash.set(data, 1);

return keccak256(toHash);
}

export function signMessage(privKeys: string[], messageToSign: Uint8Array) {
const signatures = [];

for (const privKey of privKeys) {
const { signature, recid } = secp256k1.ecdsaSign(arrayify(keccak256(messageToSign)), arrayify(privKey));
signatures.push(new Uint8Array([...signature, recid]));
}

return signatures;
}
Loading

0 comments on commit 9d36662

Please sign in to comment.