import * as ed25519 from '@noble/ed25519';
import { blake3 } from '@noble/hashes/blake3';
import { sha512 } from '@noble/hashes/sha512';
import { bytesToHex, hexToBytes } from '@noble/ciphers/utils';
import { SigilRpcClient, RpcError } from './rpc/client';
ed25519.etc.sha512Sync = (...m) => {
const h = sha512.create();
for (const part of m) h.update(part);
return h.digest();
};
const TX_DOMAIN_TAG = 'sigil/transaction/v1\n';
interface UnsignedTransaction {
chain_id: string;
sender_did: string;
nonce: number;
gas_limit: number;
gas_price: number;
tx_type: Record<string, unknown>;
timestamp: string;
}
async function signAndPack(
tx: UnsignedTransaction,
privateKey: Uint8Array,
publicKeyHex: string,
): Promise<{ tx: UnsignedTransaction; signature: string; signer_public_key: string }> {
const transcript = TX_DOMAIN_TAG + JSON.stringify(tx);
const digest = blake3(new TextEncoder().encode(transcript));
const signature = await ed25519.sign(digest, privateKey);
return { tx, signature: bytesToHex(signature), signer_public_key: publicKeyHex };
}
export async function sendPaymentWithRetry(
client: SigilRpcClient,
privateKey: Uint8Array,
publicKey: Uint8Array,
senderDid: string,
recipientDid: string,
amountBaseUnits: number,
): Promise<string> {
const { nonce } = await client.getBalance(senderDid);
const tx: UnsignedTransaction = {
chain_id: client.getNetwork().chainId,
sender_did: senderDid,
nonce,
gas_limit: 200_000,
gas_price: 1,
tx_type: {
Transfer: {
recipient_did: recipientDid,
amount: amountBaseUnits,
memo: null,
},
},
timestamp: new Date().toISOString(),
};
const signed = await signAndPack(tx, privateKey, bytesToHex(publicKey));
let delay = 250;
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
const { tx_hash } = await client.sendTransaction(signed);
return tx_hash;
} catch (err: unknown) {
if (err instanceof RpcError && (err.code === -32002 || err.code === -32603) && attempt < 4) {
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= 2;
continue;
}
throw err;
}
}
throw new Error('unreachable');
}
if (import.meta.main) {
const rpcUrl = process.env.SIGIL_RPC_URL ?? 'https://testnet-rpc.sigil.ml/rpc';
const chainId = process.env.SIGIL_CHAIN_ID ?? 'sigil-testnet-1';
const senderDid = process.env.SIGIL_SENDER_DID!;
const recipientDid = process.env.RECIPIENT_DID!;
const amount = Number(process.env.AMOUNT_BASE_UNITS ?? '0');
if (!amount) throw new Error('set AMOUNT_BASE_UNITS');
const privateKey = hexToBytes(process.env.SIGIL_SIGNING_KEY_HEX!);
const publicKey = await ed25519.getPublicKey(privateKey);
const client = new SigilRpcClient({
id: 'env',
chainId,
rpcUrl,
currencySymbol: 'SIGIL',
});
const txHash = await sendPaymentWithRetry(
client,
privateKey,
publicKey,
senderDid,
recipientDid,
amount,
);
console.log('submitted:', txHash);
}