Skip to content

Commit e296708

Browse files
committed
feat(sdk-coin-sol): add sdk-ct-v1-broadcast example for SDK-built v1 CT
Add an examples/ts/sol script that builds a Token-2022 confidential Transfer as a v1 (SIMD-0296/0385) transaction via the SDK ConfidentialTransferBuilder, consumes pre-created proof contexts (produced by the coins-sandbox ct_sdk_feed.mjs), and broadcasts + confirms on devnet. Env/state driven and credential-free. Verified on devnet: wire 0x81, 654 bytes, confirmed. Refs: CHALO-1602
1 parent bf5ca79 commit e296708

1 file changed

Lines changed: 108 additions & 0 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* CHALO-1602 Verification: SDK-built Solana v1 Confidential Transfer broadcast
3+
*
4+
* Builds a Token-2022 confidential Transfer as a v1 transaction (SIMD-0296/0385)
5+
* via the SDK ConfidentialTransferBuilder, consumes pre-created proof contexts,
6+
* and broadcasts + confirms on devnet.
7+
*
8+
* Prerequisites (produced by coins-sandbox `sol/confidentialTransfers/ct_sdk_feed.mjs`):
9+
* - sdk-e2e-input.json: source/dest ATA, mint, pre-created proof-context accounts,
10+
* and the transfer ciphertext fields (no credentials).
11+
* - extB_e2e.json: source authority secret (for signing).
12+
* - A funded devnet fee payer secret (base64 of the 64-byte ed25519 secret).
13+
*
14+
* Run:
15+
* SOL_DEVNET_RPC=<rpc> SDK_E2E_INPUT=<path/sdk-e2e-input.json> EXTB_STATE=<path/extB_e2e.json> \
16+
* PAYER_SECRET_B64=<base64> node_modules/.bin/tsx examples/ts/sol/sdk-ct-v1-broadcast.ts
17+
*
18+
* Copyright 2026, BitGo, Inc. All Rights Reserved.
19+
*/
20+
import { KeyPair, Transaction, TransactionBuilderFactory } from '@bitgo/sdk-coin-sol';
21+
import { coins } from '@bitgo/statics';
22+
import { Connection } from '@solana/web3.js';
23+
import * as bs58 from 'bs58';
24+
import { readFileSync } from 'node:fs';
25+
26+
const path = require('path');
27+
const envPath = path.resolve(__dirname, '../../../.env');
28+
require('dotenv').config({ path: envPath });
29+
30+
async function main() {
31+
const rpcUrl = process.env.SOL_DEVNET_RPC;
32+
const inputPath = process.env.SDK_E2E_INPUT;
33+
const statePath = process.env.EXTB_STATE;
34+
const payerSecretB64 = process.env.PAYER_SECRET_B64;
35+
if (!rpcUrl || !inputPath || !statePath || !payerSecretB64) {
36+
throw new Error('SOL_DEVNET_RPC, SDK_E2E_INPUT, EXTB_STATE, PAYER_SECRET_B64 env are required');
37+
}
38+
39+
const input = JSON.parse(readFileSync(inputPath, 'utf8'));
40+
const e2e = JSON.parse(readFileSync(statePath, 'utf8'));
41+
const authoritySecret = bs58.encode(Uint8Array.from(e2e.ownerSecretKey));
42+
const payerSecret = bs58.encode(Uint8Array.from(Buffer.from(payerSecretB64, 'base64')));
43+
const payerAddress = new KeyPair({ prv: payerSecret }).getKeys().pub;
44+
const authorityAddress = input.authorityAddress;
45+
46+
const connection = new Connection(rpcUrl, 'confirmed');
47+
const { blockhash } = await connection.getLatestBlockhash('finalized');
48+
49+
const factory = new TransactionBuilderFactory(coins.get('sol'));
50+
const builder = factory.getConfidentialTransferBuilder();
51+
builder.nonce(blockhash).sender(authorityAddress).feePayer(payerAddress);
52+
builder.sign({ key: payerSecret });
53+
builder.sign({ key: authoritySecret });
54+
builder.version(1).transactionConfig({
55+
computeUnitLimit: 1_400_000,
56+
heapSize: null,
57+
loadedAccountsDataSizeLimit: 1_048_576,
58+
priorityFee: 5_000,
59+
});
60+
builder.confidentialTransfer({
61+
sourceTokenAddress: input.sourceTokenAddress,
62+
mintAddress: input.mintAddress,
63+
destinationTokenAddress: input.destinationTokenAddress,
64+
authorityAddress,
65+
equalityProofContextStateAddress: input.equalityProofContextStateAddress,
66+
ciphertextValidityProofContextStateAddress: input.ciphertextValidityProofContextStateAddress,
67+
rangeProofContextStateAddress: input.rangeProofContextStateAddress,
68+
newSourceDecryptableAvailableBalance: input.newSourceDecryptableAvailableBalance,
69+
transferAmountAuditorCiphertextLo: input.transferAmountAuditorCiphertextLo,
70+
transferAmountAuditorCiphertextHi: input.transferAmountAuditorCiphertextHi,
71+
equalityProofInstructionOffset: 0,
72+
ciphertextValidityProofInstructionOffset: 0,
73+
rangeProofInstructionOffset: 0,
74+
});
75+
76+
const tx = (await builder.build()) as Transaction;
77+
const b64 = tx.toBroadcastFormat();
78+
const wire = Buffer.from(b64, 'base64');
79+
console.log('SDK-built tx isVersioned:', tx.isVersionedTransaction());
80+
console.log('wire[0] = 0x' + wire[0].toString(16) + (wire[0] === 0x81 ? ' (v1)' : ' (NOT v1!)'));
81+
console.log('wire bytes =', wire.length);
82+
if (wire[0] !== 0x81) throw new Error('SDK builder did not produce a v1 transaction');
83+
if (wire.length > 4096) throw new Error('SDK v1 tx exceeds 4096 bytes');
84+
85+
const signature = await connection.sendEncodedTransaction(b64, { encoding: 'base64', maxRetries: 3 });
86+
console.log('broadcast signature:', signature);
87+
88+
let confirmed = false;
89+
for (let i = 0; i < 30; i++) {
90+
await new Promise((r) => setTimeout(r, 2000));
91+
const status = await connection.getSignatureStatus(signature);
92+
const s = status?.value;
93+
if (s && (s.confirmationStatus === 'confirmed' || s.confirmationStatus === 'finalized')) {
94+
confirmed = true;
95+
if (s.err) throw new Error('SDK-built v1 tx failed: ' + JSON.stringify(s.err));
96+
console.log('confirmed at slot', s.slot);
97+
break;
98+
}
99+
if (s?.err) throw new Error('SDK-built v1 tx errored: ' + JSON.stringify(s.err));
100+
}
101+
if (!confirmed) throw new Error('SDK-built v1 tx not confirmed within 60s');
102+
console.log('SDK-built v1 CT Transfer CONFIRMED:', signature, '| wire bytes', wire.length);
103+
}
104+
105+
main().catch((e) => {
106+
console.error('FATAL:', e);
107+
process.exit(1);
108+
});

0 commit comments

Comments
 (0)