import * as dotenv from 'dotenv';
import * as path from 'path';
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
import { Transaction } from '@mysten/sui/transactions';
import { Ed25519PublicKey } from '@mysten/sui/keypairs/ed25519';
import { messageWithIntent } from '@mysten/sui/cryptography';
import { Turnkey } from '@turnkey/sdk-server';
import { blake2b } from '@noble/hashes/blake2b';
import { bytesToHex } from '@noble/hashes/utils';
dotenv.config({ path: path.resolve(process.cwd(), '.env.local') });
function toSerializedSignature({
signature,
pubKey,
}: {
signature: Uint8Array;
pubKey: Ed25519PublicKey;
}): string {
const scheme = new Uint8Array([0x00]); // ED25519 flag
const pubKeyBytes = pubKey.toRawBytes();
const serialized = new Uint8Array(
scheme.length + signature.length + pubKeyBytes.length
);
serialized.set(scheme, 0);
serialized.set(signature, scheme.length);
serialized.set(pubKeyBytes, scheme.length + signature.length);
return Buffer.from(serialized).toString('base64');
}
async function main() {
// load the variables from .env
// SUI_ADDRESS and SUI_PUBLIC_KEY of the Turnkey signer
const {
ORGANIZATION_ID,
API_PRIVATE_KEY,
API_PUBLIC_KEY,
SUI_ADDRESS,
SUI_PUBLIC_KEY,
} = process.env;
// sending to the same address
const recipient = SUI_ADDRESS;
const amount = 1_000_000n; // 0.001 SUI
const turnkeyClient = new Turnkey({
apiBaseUrl: 'https://api.turnkey.com',
apiPrivateKey: API_PRIVATE_KEY!,
apiPublicKey: API_PUBLIC_KEY!,
defaultOrganizationId: ORGANIZATION_ID!,
});
const provider = new SuiClient({ url: getFullnodeUrl('testnet') });
const publicKey = new Ed25519PublicKey(Buffer.from(SUI_PUBLIC_KEY!, 'hex'));
if (publicKey.toSuiAddress() !== SUI_ADDRESS) {
throw new Error('SUI_PUBLIC_KEY does not match SUI_ADDRESS');
}
// fetch the user's SUI coin objects
const coins = await provider.getCoins({
owner: SUI_ADDRESS!,
coinType: '0x2::sui::SUI',
});
if (!coins.data.length) throw new Error('No SUI coins');
const tx = new Transaction();
tx.setSender(SUI_ADDRESS!);
tx.setGasPrice(await provider.getReferenceGasPrice());
tx.setGasBudget(5_000_000n);
tx.setGasPayment([
{
objectId: coins.data[0]!.coinObjectId,
version: coins.data[0]!.version,
digest: coins.data[0]!.digest,
},
]);
const coin = tx.splitCoins(tx.gas, [tx.pure('u64', amount)]);
tx.transferObjects([coin], tx.pure.address(recipient));
const txBytes = await tx.build();
const intentMsg = messageWithIntent('TransactionData', txBytes);
const digest = blake2b(intentMsg, { dkLen: 32 });
const { r, s } = await turnkeyClient.apiClient().signRawPayload({
signWith: SUI_ADDRESS!,
payload: bytesToHex(digest),
encoding: 'PAYLOAD_ENCODING_HEXADECIMAL',
hashFunction: 'HASH_FUNCTION_NOT_APPLICABLE',
});
const signature = Buffer.from(r + s, 'hex');
const serialized = toSerializedSignature({ signature, pubKey: publicKey });
const result = await provider.executeTransactionBlock({
transactionBlock: Buffer.from(txBytes).toString('base64'),
signature: serialized,
requestType: 'WaitForEffectsCert',
options: { showEffects: true },
});
console.log('Transaction digest:', result.digest);
}
main().catch((err) => {
console.error('Error:', err);
process.exit(1);
});