@unicitylabs/bridge-plugin 0.2.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/DEMO.md +255 -0
  2. package/README.md +89 -0
  3. package/lib/BridgeLockJustification.d.ts +31 -0
  4. package/lib/BridgeLockJustification.js +41 -0
  5. package/lib/BridgeMintJustificationVerifier.d.ts +12 -0
  6. package/lib/BridgeMintJustificationVerifier.js +35 -0
  7. package/lib/LockMintJustificationVerifier.d.ts +42 -0
  8. package/lib/LockMintJustificationVerifier.js +108 -0
  9. package/lib/address.d.ts +13 -0
  10. package/lib/address.js +71 -0
  11. package/lib/bridge-back/abi.d.ts +30 -0
  12. package/lib/bridge-back/abi.js +89 -0
  13. package/lib/bridge-back/burn.d.ts +79 -0
  14. package/lib/bridge-back/burn.js +78 -0
  15. package/lib/bridge-back/burned-blob.d.ts +6 -0
  16. package/lib/bridge-back/burned-blob.js +16 -0
  17. package/lib/bridge-back/cbor.d.ts +32 -0
  18. package/lib/bridge-back/cbor.js +146 -0
  19. package/lib/bridge-back/derivations.d.ts +112 -0
  20. package/lib/bridge-back/derivations.js +201 -0
  21. package/lib/bridge-back/index.d.ts +11 -0
  22. package/lib/bridge-back/index.js +11 -0
  23. package/lib/cli/main.d.ts +2 -0
  24. package/lib/cli/main.js +149 -0
  25. package/lib/cli/scenario.d.ts +36 -0
  26. package/lib/cli/scenario.js +94 -0
  27. package/lib/config.d.ts +25 -0
  28. package/lib/config.js +1 -0
  29. package/lib/contract-call.d.ts +13 -0
  30. package/lib/contract-call.js +19 -0
  31. package/lib/evm/EvmRpcClient.d.ts +25 -0
  32. package/lib/evm/EvmRpcClient.js +54 -0
  33. package/lib/evm/config.d.ts +6 -0
  34. package/lib/evm/config.js +6 -0
  35. package/lib/evm/family.d.ts +3 -0
  36. package/lib/evm/family.js +21 -0
  37. package/lib/evm/index.d.ts +6 -0
  38. package/lib/evm/index.js +6 -0
  39. package/lib/evm/presentation.d.ts +4 -0
  40. package/lib/evm/presentation.js +21 -0
  41. package/lib/evm/providers.d.ts +3 -0
  42. package/lib/evm/providers.js +9 -0
  43. package/lib/evm/signer.d.ts +43 -0
  44. package/lib/evm/signer.js +91 -0
  45. package/lib/families.d.ts +4 -0
  46. package/lib/families.js +11 -0
  47. package/lib/family.d.ts +16 -0
  48. package/lib/family.js +1 -0
  49. package/lib/hex.d.ts +4 -0
  50. package/lib/hex.js +33 -0
  51. package/lib/identifiers.d.ts +14 -0
  52. package/lib/identifiers.js +22 -0
  53. package/lib/index.d.ts +48 -0
  54. package/lib/index.js +55 -0
  55. package/lib/lock-event.d.ts +28 -0
  56. package/lib/lock-event.js +48 -0
  57. package/lib/source-chain.d.ts +29 -0
  58. package/lib/source-chain.js +1 -0
  59. package/lib/tron/TronRpcClient.d.ts +39 -0
  60. package/lib/tron/TronRpcClient.js +100 -0
  61. package/lib/tron/config.d.ts +11 -0
  62. package/lib/tron/config.js +31 -0
  63. package/lib/tron/family.d.ts +4 -0
  64. package/lib/tron/family.js +18 -0
  65. package/lib/tron/presentation.d.ts +10 -0
  66. package/lib/tron/presentation.js +18 -0
  67. package/lib/tron/providers.d.ts +46 -0
  68. package/lib/tron/providers.js +32 -0
  69. package/lib/tron/signer.d.ts +166 -0
  70. package/lib/tron/signer.js +238 -0
  71. package/lib/value.d.ts +23 -0
  72. package/lib/value.js +58 -0
  73. package/lib/wallet/allowance.d.ts +20 -0
  74. package/lib/wallet/allowance.js +27 -0
  75. package/lib/wallet/backing.d.ts +2 -0
  76. package/lib/wallet/backing.js +15 -0
  77. package/lib/wallet/facade.d.ts +97 -0
  78. package/lib/wallet/facade.js +100 -0
  79. package/lib/wallet/finality.d.ts +8 -0
  80. package/lib/wallet/finality.js +28 -0
  81. package/lib/wallet/index.d.ts +25 -0
  82. package/lib/wallet/index.js +25 -0
  83. package/lib/wallet/manifest.d.ts +69 -0
  84. package/lib/wallet/manifest.js +81 -0
  85. package/lib/wallet/manifests.d.ts +31 -0
  86. package/lib/wallet/manifests.js +70 -0
  87. package/lib/wallet/payout.d.ts +5 -0
  88. package/lib/wallet/payout.js +14 -0
  89. package/lib/wallet/registry.d.ts +40 -0
  90. package/lib/wallet/registry.js +34 -0
  91. package/lib/wallet/return-client.d.ts +91 -0
  92. package/lib/wallet/return-client.js +100 -0
  93. package/lib/wallet/self-mint-verifier.d.ts +10 -0
  94. package/lib/wallet/self-mint-verifier.js +5 -0
  95. package/lib/wallet/signer.d.ts +18 -0
  96. package/lib/wallet/signer.js +1 -0
  97. package/lib/wallet/source-adapter.d.ts +33 -0
  98. package/lib/wallet/source-adapter.js +99 -0
  99. package/lib/wallet/token-plugin.d.ts +4 -0
  100. package/lib/wallet/token-plugin.js +20 -0
  101. package/manifests/bridges.nile.json +23 -0
  102. package/package.json +71 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The bridge-back derivations from protocol/interop.md.
3
+ * Each function is the TS half of one cross-stack value and reproduces the Rust
4
+ * reference (`protocol/vectors/gen/src/derive.rs`) byte-for-byte. The wallet uses
5
+ * these read-only to show the user the pending nullifier / release leaf, and a
6
+ * TS relayer reuses them; the prover (Rust) and the vault (Solidity) recompute
7
+ * the same bytes (00 §2–7).
8
+ */
9
+ import { sha256 } from '@noble/hashes/sha2.js';
10
+ import * as abi from './abi.js';
11
+ import * as cbor from './cbor.js';
12
+ // Domain separators (00 §2, §3, §5, §7) — must match derive.rs exactly.
13
+ export const DOMAIN_CONFIG = 'unicity-bridge-return-config:v1';
14
+ export const DOMAIN_LOCK = 'unicity-bridge-lock:v1';
15
+ export const DOMAIN_RETURN = 'unicity-bridge-return:v1';
16
+ export const DOMAIN_BURN_TRANSITION = 'unicity-burn-transition:v1';
17
+ export const DOMAIN_NULLIFIER = 'unicity-bridge-return-nullifier:v1';
18
+ /** 32-byte big-endian representation of an unsigned integer. */
19
+ export function u256be(n) {
20
+ if (n < 0n)
21
+ throw new Error('u256be: negative');
22
+ const out = new Uint8Array(32);
23
+ for (let i = 0; i < 32 && n > 0n; i++) {
24
+ out[31 - i] = Number(n & 0xffn);
25
+ n >>= 8n;
26
+ }
27
+ if (n > 0n)
28
+ throw new Error('u256be: overflow');
29
+ return out;
30
+ }
31
+ /** `configHash = K(abi.encode("...config:v1", fields...))` (00 §2). */
32
+ export function configHash(c) {
33
+ return abi.keccak256(abi.encode([
34
+ abi.Str(DOMAIN_CONFIG),
35
+ abi.U64(c.sourceChainId),
36
+ abi.Addr(c.vault),
37
+ abi.Addr(c.asset),
38
+ abi.B32(c.tokenType),
39
+ abi.B32(c.coinId),
40
+ abi.U64(c.reasonTag),
41
+ abi.B32(c.lockDomain),
42
+ abi.B32(c.nullifierDomain),
43
+ ]));
44
+ }
45
+ /** `domainTag = K("unicity-bridge-return:v1")` over raw bytes (00 §7). */
46
+ export function domainTag() {
47
+ return abi.keccak256(new TextEncoder().encode(DOMAIN_RETURN));
48
+ }
49
+ /** `lockDigest = K(abi.encode("...lock:v1", fields...))` (00 §3). */
50
+ export function lockDigest(params) {
51
+ return abi.keccak256(abi.encode([
52
+ abi.Str(DOMAIN_LOCK),
53
+ abi.U64(params.sourceChainId),
54
+ abi.Addr(params.vault),
55
+ abi.U256(params.nonce),
56
+ abi.Addr(params.asset),
57
+ abi.B32(params.tokenType),
58
+ abi.B32(params.coinId),
59
+ abi.U256(params.amount),
60
+ abi.B32(params.unicityTokenId),
61
+ abi.B32(params.recipientCommitment),
62
+ ]));
63
+ }
64
+ /**
65
+ * Canonical CBOR of the 11-field `BridgeBackReason` array under `reasonTag`
66
+ * (00 §4). This is the `reasonBytes` that ride in the terminal burn's auxiliary
67
+ * data; the burn predicate binds `H(reasonBytes)` (see {reasonHash}).
68
+ */
69
+ export function encodeBridgeBackReason(c, r) {
70
+ return cbor.concatBytes([
71
+ cbor.tag(c.reasonTag),
72
+ cbor.arrayHeader(11),
73
+ cbor.uint(r.version),
74
+ cbor.uint(c.sourceChainId),
75
+ cbor.bytes(c.vault),
76
+ cbor.bytes(c.asset),
77
+ cbor.bytes(c.tokenType),
78
+ cbor.bytes(c.coinId),
79
+ cbor.bytes(r.recipient),
80
+ cbor.bytes(cbor.minimalBe(u256be(r.amount))),
81
+ cbor.bytes(r.feeRecipient),
82
+ cbor.bytes(cbor.minimalBe(u256be(r.feeAmount))),
83
+ cbor.uint(r.deadline),
84
+ ]);
85
+ }
86
+ /** `reasonHash = H(reasonBytes)` — the value `BurnPredicate(H(reasonBytes))`
87
+ * binds (00 §4, PROVISIONAL preimage pending M0 SDK confirmation). */
88
+ export function reasonHash(reasonBytes) {
89
+ return sha256(reasonBytes);
90
+ }
91
+ /**
92
+ * Inverse of {encodeBridgeBackReason}: decode the canonical `reasonBytes` a
93
+ * burned token blob carries (the circuit performs the same decode). Strict —
94
+ * rejects non-canonical CBOR and any trailing bytes (00 §4). `amount`/`feeAmount`
95
+ * come back as the minimal-big-endian byte strings re-read as integers.
96
+ */
97
+ export function decodeBridgeBackReason(reasonBytes) {
98
+ const r = new cbor.CborReader(reasonBytes);
99
+ const reasonTag = r.readTag();
100
+ const n = r.readArrayHeader();
101
+ if (n !== 11)
102
+ throw new Error(`BridgeBackReason: expected 11 fields, got ${n}`);
103
+ const version = r.readUint();
104
+ const sourceChainId = r.readUint();
105
+ const vault = r.readBytes();
106
+ const asset = r.readBytes();
107
+ const tokenType = r.readBytes();
108
+ const coinId = r.readBytes();
109
+ const recipient = r.readBytes();
110
+ const amount = beToBigInt(r.readBytes());
111
+ const feeRecipient = r.readBytes();
112
+ const feeAmount = beToBigInt(r.readBytes());
113
+ const deadline = r.readUint();
114
+ if (!r.done)
115
+ throw new Error('BridgeBackReason: trailing bytes after reason');
116
+ return {
117
+ reasonTag,
118
+ version,
119
+ sourceChainId,
120
+ vault,
121
+ asset,
122
+ tokenType,
123
+ coinId,
124
+ recipient,
125
+ amount,
126
+ feeRecipient,
127
+ feeAmount,
128
+ deadline,
129
+ };
130
+ }
131
+ function beToBigInt(b) {
132
+ let n = 0n;
133
+ for (const x of b)
134
+ n = (n << 8n) | BigInt(x);
135
+ return n;
136
+ }
137
+ /** `burnTransitionId = H("unicity-burn-transition:v1", stateId, txHash)` (00 §5). */
138
+ export function burnTransitionId(stateId, txHash) {
139
+ return cbor.hArray([cbor.text(DOMAIN_BURN_TRANSITION), cbor.bytes(stateId), cbor.bytes(txHash)]);
140
+ }
141
+ /** `nullifier = H("...nullifier:v1", configHash, burnTransitionId)` (00 §5). */
142
+ export function nullifier(configHashBytes, burnTransitionIdBytes) {
143
+ return cbor.hArray([
144
+ cbor.text(DOMAIN_NULLIFIER),
145
+ cbor.bytes(configHashBytes),
146
+ cbor.bytes(burnTransitionIdBytes),
147
+ ]);
148
+ }
149
+ function leafWords(l) {
150
+ return [
151
+ abi.B32(l.nullifier),
152
+ abi.Addr(l.recipient),
153
+ abi.U256(l.amount),
154
+ abi.Addr(l.feeRecipient),
155
+ abi.U256(l.feeAmount),
156
+ abi.U64(l.deadline),
157
+ ];
158
+ }
159
+ /** `returnRoot = K( concat of fixed-width abi.encode(leaf) )` (00 §7). */
160
+ export function returnRoot(leaves) {
161
+ const parts = leaves.map((l) => abi.packWords(leafWords(l)));
162
+ return abi.keccak256(concatU8(parts));
163
+ }
164
+ /** `lockRefRoot = K( concat of fixed-width abi.encode(ref) )`, sorted by nonce
165
+ * with duplicates rejected (00 §7). */
166
+ export function lockRefRoot(refs) {
167
+ const sorted = [...refs].sort((a, b) => (a.nonce < b.nonce ? -1 : a.nonce > b.nonce ? 1 : 0));
168
+ for (let i = 1; i < sorted.length; i++) {
169
+ if (sorted[i].nonce === sorted[i - 1].nonce) {
170
+ throw new Error(`lockRefRoot: duplicate nonce ${sorted[i].nonce}`);
171
+ }
172
+ }
173
+ const parts = sorted.map((r) => abi.packWords([abi.U256(r.nonce), abi.B32(r.digest)]));
174
+ return abi.keccak256(concatU8(parts));
175
+ }
176
+ /** `abi.encode(PublicValues)` — the all-static layout the vault abi.decodes (00 §7). */
177
+ export function publicValuesAbi(p) {
178
+ return abi.encode([
179
+ abi.B32(p.domainTag),
180
+ abi.B32(p.configHash),
181
+ abi.B32(p.trustBaseHash),
182
+ abi.B32(p.spentRootOld),
183
+ abi.B32(p.spentRootNew),
184
+ abi.B32(p.returnRoot),
185
+ abi.B32(p.lockRefRoot),
186
+ abi.U32(p.batchSize),
187
+ abi.U256(p.totalAmount),
188
+ ]);
189
+ }
190
+ function concatU8(parts) {
191
+ let len = 0;
192
+ for (const p of parts)
193
+ len += p.length;
194
+ const out = new Uint8Array(len);
195
+ let off = 0;
196
+ for (const p of parts) {
197
+ out.set(p, off);
198
+ off += p.length;
199
+ }
200
+ return out;
201
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Bridge-back construction (02-ts-sdk-and-wallet.md Part 2): chain-agnostic,
3
+ * authority-free derivations the wallet/relayer produces for the return path.
4
+ * Every byte here is consumed by the prover (Rust) and the vault (Solidity), so
5
+ * it is conformance-tested against `protocol/vectors` (interop §10).
6
+ */
7
+ export * as bridgeCbor from './cbor.js';
8
+ export * as bridgeAbi from './abi.js';
9
+ export * from './derivations.js';
10
+ export * from './burn.js';
11
+ export * from './burned-blob.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Bridge-back construction (02-ts-sdk-and-wallet.md Part 2): chain-agnostic,
3
+ * authority-free derivations the wallet/relayer produces for the return path.
4
+ * Every byte here is consumed by the prover (Rust) and the vault (Solidity), so
5
+ * it is conformance-tested against `protocol/vectors` (interop §10).
6
+ */
7
+ export * as bridgeCbor from './cbor.js';
8
+ export * as bridgeAbi from './abi.js';
9
+ export * from './derivations.js';
10
+ export * from './burn.js';
11
+ export * from './burned-blob.js';
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ import { CertifiedMintTransaction } from '@unicitylabs/state-transition-sdk/lib/transaction/CertifiedMintTransaction.js';
3
+ import { VerificationStatus } from '@unicitylabs/state-transition-sdk/lib/verification/VerificationStatus.js';
4
+ import { createBridgePlugin, TRON_MAINNET_CHAIN_ID, TRON_NILE_CHAIN_ID, TRON_MAINNET_USDT, } from '../index.js';
5
+ import { buildDemo, hexToBytes } from './scenario.js';
6
+ const AMOUNT = 1000000n; // 1 USDT (6 decimals)
7
+ const CONFIRMATIONS = 20;
8
+ // Nile demo config (addresses are illustrative for the offline demo).
9
+ const DEMO_CONFIG = {
10
+ family: 'tron',
11
+ chainId: TRON_NILE_CHAIN_ID,
12
+ lockContract: '41' + 'ab'.repeat(20),
13
+ assetContract: '41' + 'de'.repeat(20),
14
+ confirmations: CONFIRMATIONS,
15
+ decimals: 6,
16
+ };
17
+ const noNestedTokens = () => { };
18
+ async function run(build) {
19
+ const r = await build.plugin.verifier.verify(build.certifiedTx, noNestedTokens);
20
+ return { status: r.status, message: r.message };
21
+ }
22
+ async function demo() {
23
+ console.log('Unicity ⇽ Tron USDT bridge — offline security demo');
24
+ console.log('(mock Tron RPC; mint reason = self-contained lock proof)\n');
25
+ const checks = [
26
+ {
27
+ name: 'Valid bridged mint (lock finalized, bound to this token+recipient)',
28
+ expect: VerificationStatus.OK,
29
+ build: () => buildDemo(DEMO_CONFIG, AMOUNT, CONFIRMATIONS).then(run),
30
+ },
31
+ {
32
+ name: 'Attack: inflate token value above locked amount',
33
+ expect: VerificationStatus.FAIL,
34
+ build: () => buildDemo(DEMO_CONFIG, AMOUNT, CONFIRMATIONS, { tokenValueAmount: AMOUNT * 1000n }).then(run),
35
+ },
36
+ {
37
+ name: 'Attack: tamper justification amount',
38
+ expect: VerificationStatus.FAIL,
39
+ build: () => buildDemo(DEMO_CONFIG, AMOUNT, CONFIRMATIONS, {
40
+ justification: (d) => ({ ...d, amount: AMOUNT + 1n }),
41
+ }).then(run),
42
+ },
43
+ {
44
+ name: 'Attack: replay lock for a different token id',
45
+ expect: VerificationStatus.FAIL,
46
+ build: () => buildDemo(DEMO_CONFIG, AMOUNT, CONFIRMATIONS, {
47
+ logs: (def) => [{ ...def, data: def.data.slice(0, 64) + '99'.repeat(32) + def.data.slice(128) }],
48
+ }).then(run),
49
+ },
50
+ {
51
+ name: 'Attack: steal lock by swapping recipient',
52
+ expect: VerificationStatus.FAIL,
53
+ build: () => buildDemo(DEMO_CONFIG, AMOUNT, CONFIRMATIONS, {
54
+ logs: (def) => [{ ...def, data: def.data.slice(0, 128) + '88'.repeat(32) }],
55
+ }).then(run),
56
+ },
57
+ {
58
+ name: 'Attack: forged lock contract emits the event',
59
+ expect: VerificationStatus.FAIL,
60
+ build: () => buildDemo(DEMO_CONFIG, AMOUNT, CONFIRMATIONS, {
61
+ logs: (def) => [{ ...def, address: 'ff'.repeat(20) }],
62
+ }).then(run),
63
+ },
64
+ {
65
+ name: 'Attack: spend lock before finality (insufficient confirmations)',
66
+ expect: VerificationStatus.FAIL,
67
+ build: () => buildDemo(DEMO_CONFIG, AMOUNT, CONFIRMATIONS, {
68
+ blockNumber: 1000n,
69
+ tip: 1000n + BigInt(CONFIRMATIONS) - 1n,
70
+ }).then(run),
71
+ },
72
+ ];
73
+ let failures = 0;
74
+ for (const check of checks) {
75
+ const { status, message } = await check.build();
76
+ const ok = status === check.expect;
77
+ if (!ok) {
78
+ failures++;
79
+ }
80
+ const tag = ok ? '✔' : '✘ UNEXPECTED';
81
+ const verdict = status === VerificationStatus.OK ? 'OK ' : 'FAIL';
82
+ console.log(`${tag} [${verdict}] ${check.name}`);
83
+ if (status === VerificationStatus.FAIL && message) {
84
+ console.log(` ↳ ${message}`);
85
+ }
86
+ }
87
+ console.log('');
88
+ if (failures === 0) {
89
+ console.log('All checks behaved as expected: valid token accepted, every attack rejected.');
90
+ return 0;
91
+ }
92
+ console.error(`${failures} check(s) did not behave as expected.`);
93
+ return 1;
94
+ }
95
+ async function verifyOnChain(args) {
96
+ const tokenHex = args.get('token');
97
+ const lockContract = args.get('lock');
98
+ const rpcUrl = args.get('rpc');
99
+ if (!tokenHex || !lockContract || !rpcUrl) {
100
+ console.error('Usage: tron-usdt-bridge verify --token <hex> --lock <addr> --rpc <url> [--asset <addr>] [--chain mainnet|nile] [--api-key <key>] [--confirmations N]');
101
+ return 2;
102
+ }
103
+ const chain = (args.get('chain') ?? 'mainnet') === 'nile' ? TRON_NILE_CHAIN_ID : TRON_MAINNET_CHAIN_ID;
104
+ const config = {
105
+ family: 'tron',
106
+ chainId: chain,
107
+ lockContract,
108
+ assetContract: args.get('asset') ?? TRON_MAINNET_USDT,
109
+ confirmations: Number(args.get('confirmations') ?? CONFIRMATIONS),
110
+ decimals: 6,
111
+ rpcUrl,
112
+ apiKey: args.get('api-key'),
113
+ };
114
+ // Bridge tokens carry value in the wallet's format (SpherePaymentData, tag 39050); see src/value.ts.
115
+ const plugin = createBridgePlugin(config);
116
+ const tx = await CertifiedMintTransaction.fromCBOR(hexToBytes(tokenHex));
117
+ const result = await plugin.verifier.verify(tx, noNestedTokens);
118
+ console.log(`Token type expected for this asset: ${plugin.tokenTypeHex}`);
119
+ console.log(`Verification: ${result.status}${result.message ? ` — ${result.message}` : ''}`);
120
+ return result.status === VerificationStatus.OK ? 0 : 1;
121
+ }
122
+ function parseArgs(argv) {
123
+ const map = new Map();
124
+ for (let i = 0; i < argv.length; i++) {
125
+ if (argv[i].startsWith('--')) {
126
+ const key = argv[i].slice(2);
127
+ const val = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[(i += 1)] : 'true';
128
+ map.set(key, val);
129
+ }
130
+ }
131
+ return map;
132
+ }
133
+ async function main() {
134
+ const [cmd, ...rest] = process.argv.slice(2);
135
+ let code;
136
+ switch (cmd ?? 'demo') {
137
+ case 'demo':
138
+ code = await demo();
139
+ break;
140
+ case 'verify':
141
+ code = await verifyOnChain(parseArgs(rest));
142
+ break;
143
+ default:
144
+ console.error(`Unknown command: ${cmd}. Use "demo" or "verify".`);
145
+ code = 2;
146
+ }
147
+ process.exit(code);
148
+ }
149
+ void main();
@@ -0,0 +1,36 @@
1
+ import { NetworkId } from '@unicitylabs/state-transition-sdk/lib/api/NetworkId.js';
2
+ import type { CertifiedMintTransaction } from '@unicitylabs/state-transition-sdk/lib/transaction/CertifiedMintTransaction.js';
3
+ import { BridgeLockJustification, type SourceLog, type SourceChainRpc, type SourceTxInfo, type BridgeAssetConfig, type BridgePlugin } from '../index.js';
4
+ export declare class MockTronRpc implements SourceChainRpc {
5
+ txInfo: SourceTxInfo | null;
6
+ tip: bigint;
7
+ constructor(txInfo: SourceTxInfo | null, tip: bigint);
8
+ getTransactionInfo(): Promise<SourceTxInfo | null>;
9
+ getNowBlockNumber(): Promise<bigint>;
10
+ }
11
+ export interface LockEventFields {
12
+ nonce: bigint;
13
+ fromEvmHex: string;
14
+ amount: bigint;
15
+ unicityTokenId: Uint8Array;
16
+ recipientCommitment: Uint8Array;
17
+ }
18
+ export declare function makeLockLog(addressEvmHex: string, e: LockEventFields): SourceLog;
19
+ export declare const DEMO_NETWORK: NetworkId;
20
+ export declare const DEMO_TXID: string;
21
+ export declare const DEMO_NONCE = 7n;
22
+ export interface DemoBuild {
23
+ plugin: BridgePlugin;
24
+ certifiedTx: CertifiedMintTransaction;
25
+ rpc: MockTronRpc;
26
+ tokenIdHex: string;
27
+ }
28
+ /** Builds a valid lock + matching bridged mint, then applies optional tampering. */
29
+ export declare function buildDemo(config: BridgeAssetConfig, amount: bigint, confirmations: number, overrides?: {
30
+ logs?: (def: SourceLog) => SourceLog[];
31
+ justification?: (d: BridgeLockJustification['data']) => BridgeLockJustification['data'];
32
+ tokenValueAmount?: bigint;
33
+ tip?: bigint;
34
+ blockNumber?: bigint;
35
+ }): Promise<DemoBuild>;
36
+ export declare function hexToBytes(hex: string): Uint8Array;
@@ -0,0 +1,94 @@
1
+ import { NetworkId } from '@unicitylabs/state-transition-sdk/lib/api/NetworkId.js';
2
+ import { SigningService } from '@unicitylabs/state-transition-sdk/lib/crypto/secp256k1/SigningService.js';
3
+ import { EncodedPredicate } from '@unicitylabs/state-transition-sdk/lib/predicate/EncodedPredicate.js';
4
+ import { SignaturePredicate } from '@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicate.js';
5
+ import { MintTransaction } from '@unicitylabs/state-transition-sdk/lib/transaction/MintTransaction.js';
6
+ import { TokenId } from '@unicitylabs/state-transition-sdk/lib/transaction/TokenId.js';
7
+ import { TokenSalt } from '@unicitylabs/state-transition-sdk/lib/transaction/TokenSalt.js';
8
+ import { TokenType } from '@unicitylabs/state-transition-sdk/lib/transaction/TokenType.js';
9
+ import { createBridgePlugin, encodeBridgePaymentData, LOCK_EVENT_TOPIC0, recipientCommitment, toHex, BridgeLockJustification, } from '../index.js';
10
+ export class MockTronRpc {
11
+ txInfo;
12
+ tip;
13
+ constructor(txInfo, tip) {
14
+ this.txInfo = txInfo;
15
+ this.tip = tip;
16
+ }
17
+ async getTransactionInfo() {
18
+ return this.txInfo;
19
+ }
20
+ async getNowBlockNumber() {
21
+ return this.tip;
22
+ }
23
+ }
24
+ function word(v) {
25
+ if (v instanceof Uint8Array) {
26
+ return toHex(v).padStart(64, '0');
27
+ }
28
+ return v.toString(16).padStart(64, '0');
29
+ }
30
+ export function makeLockLog(addressEvmHex, e) {
31
+ return {
32
+ address: addressEvmHex.toLowerCase(),
33
+ topics: [LOCK_EVENT_TOPIC0, word(e.nonce), e.fromEvmHex.toLowerCase().padStart(64, '0')],
34
+ data: word(e.amount) + word(e.unicityTokenId) + word(e.recipientCommitment),
35
+ };
36
+ }
37
+ export const DEMO_NETWORK = NetworkId.fromId(4);
38
+ export const DEMO_TXID = 'a1'.repeat(32);
39
+ export const DEMO_NONCE = 7n;
40
+ /** Builds a valid lock + matching bridged mint, then applies optional tampering. */
41
+ export async function buildDemo(config, amount, confirmations, overrides = {}) {
42
+ const recipient = SignaturePredicate.create(SigningService.generate().publicKey);
43
+ const recipientCommitmentBytes = recipientCommitment(EncodedPredicate.fromPredicate(recipient).toCBOR());
44
+ const salt = TokenSalt.generate();
45
+ const tokenId = await TokenId.fromSalt(DEMO_NETWORK, salt);
46
+ const blockNumber = overrides.blockNumber ?? 1000n;
47
+ const tip = overrides.tip ?? blockNumber + BigInt(confirmations);
48
+ // Build a probe plugin only to learn the normalized lock-contract hex.
49
+ const probe = createBridgePlugin(config, { rpc: new MockTronRpc(null, 0n) });
50
+ const defaultLog = makeLockLog(probe.resolvedConfig.lockContractHex, {
51
+ nonce: DEMO_NONCE,
52
+ fromEvmHex: 'cc'.repeat(20),
53
+ amount,
54
+ unicityTokenId: tokenId.bytes,
55
+ recipientCommitment: recipientCommitmentBytes,
56
+ });
57
+ const logs = overrides.logs ? overrides.logs(defaultLog) : [defaultLog];
58
+ const rpc = new MockTronRpc({ blockNumber, success: true, logs }, tip);
59
+ const plugin = createBridgePlugin(config, { rpc });
60
+ let jData = {
61
+ chainId: config.chainId,
62
+ lockContract: hexToBytes(plugin.resolvedConfig.lockContractHex),
63
+ assetContract: hexToBytes(plugin.resolvedConfig.assetContractHex),
64
+ txid: hexToBytes(DEMO_TXID),
65
+ logIndex: 0,
66
+ amount,
67
+ nonce: DEMO_NONCE,
68
+ };
69
+ if (overrides.justification) {
70
+ jData = overrides.justification(jData);
71
+ }
72
+ const valueData = encodeBridgePaymentData(plugin.resolvedConfig.coinId, overrides.tokenValueAmount ?? amount);
73
+ const tokenType = new TokenType(plugin.resolvedConfig.tokenType);
74
+ const mint = await MintTransaction.create(DEMO_NETWORK, recipient, {
75
+ data: valueData,
76
+ tokenType,
77
+ salt,
78
+ justification: new BridgeLockJustification(jData).toCBOR(),
79
+ });
80
+ return {
81
+ plugin,
82
+ certifiedTx: mint,
83
+ rpc,
84
+ tokenIdHex: toHex(tokenId.bytes),
85
+ };
86
+ }
87
+ export function hexToBytes(hex) {
88
+ const h = hex.startsWith('0x') ? hex.slice(2) : hex;
89
+ const out = new Uint8Array(h.length / 2);
90
+ for (let i = 0; i < out.length; i++) {
91
+ out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
92
+ }
93
+ return out;
94
+ }
@@ -0,0 +1,25 @@
1
+ import type { ChainFamily } from '@unicitylabs/bridge-core';
2
+ /**
3
+ * Configuration for one bridged asset. The family, chain, contract and asset
4
+ * fields are trust anchors: the verifier rejects any lock proof that does not
5
+ * match them.
6
+ */
7
+ export interface BridgeAssetConfig {
8
+ /** Source chain family (CAIP-2 namespace). */
9
+ readonly family: ChainFamily;
10
+ /** Source chain id in the family's numbering. */
11
+ readonly chainId: number;
12
+ /** Canonical vault (lock) contract, in any of the family's address forms. */
13
+ readonly lockContract: string;
14
+ /** Bridged token contract, same forms. */
15
+ readonly assetContract: string;
16
+ /** Required confirmations for source finality; the family's default when omitted. */
17
+ readonly confirmations?: number;
18
+ /** Token decimals (USDT and USDC: 6). */
19
+ readonly decimals?: number;
20
+ /** Node URL for the family's RPC client (used when no client is injected). */
21
+ readonly rpcUrl?: string;
22
+ /** Node API key, where the family's node wants one (TronGrid). */
23
+ readonly apiKey?: string;
24
+ }
25
+ export declare const DEFAULT_DECIMALS = 6;
package/lib/config.js ADDED
@@ -0,0 +1 @@
1
+ export const DEFAULT_DECIMALS = 6;
@@ -0,0 +1,13 @@
1
+ export interface CallParam {
2
+ readonly type: 'address' | 'uint256' | 'bytes32';
3
+ readonly value: string;
4
+ }
5
+ export interface ContractCall {
6
+ readonly contractHex: string;
7
+ /** Solidity function signature, e.g. `lock(uint256,bytes32,bytes32)`. */
8
+ readonly functionSignature: string;
9
+ /** Typed parameters in declaration order. */
10
+ readonly parameters: readonly CallParam[];
11
+ }
12
+ export declare function selectorHex(functionSignature: string): string;
13
+ export declare function encodeCallData(call: ContractCall): string;
@@ -0,0 +1,19 @@
1
+ import { keccak_256 } from '@noble/hashes/sha3.js';
2
+ import { Addr, B32, U256, encode } from './bridge-back/abi.js';
3
+ import { fromHex, toHex } from './hex.js';
4
+ export function selectorHex(functionSignature) {
5
+ return toHex(keccak_256(new TextEncoder().encode(functionSignature)).subarray(0, 4));
6
+ }
7
+ function paramValue(p) {
8
+ switch (p.type) {
9
+ case 'address':
10
+ return Addr(fromHex(p.value));
11
+ case 'bytes32':
12
+ return B32(fromHex(p.value));
13
+ case 'uint256':
14
+ return U256(BigInt(p.value));
15
+ }
16
+ }
17
+ export function encodeCallData(call) {
18
+ return selectorHex(call.functionSignature) + toHex(encode(call.parameters.map(paramValue)));
19
+ }
@@ -0,0 +1,25 @@
1
+ import type { ConstantCallInput, ConstantCaller, SourceChainRpc, SourceTxInfo } from '../source-chain.js';
2
+ type FetchLike = (input: string, init?: {
3
+ method: string;
4
+ headers: Record<string, string>;
5
+ body: string;
6
+ }) => Promise<{
7
+ ok: boolean;
8
+ status: number;
9
+ json: () => Promise<unknown>;
10
+ }>;
11
+ export interface EvmJsonRpcClientOptions {
12
+ readonly rpcUrl: string;
13
+ /** Injectable fetch (defaults to globalThis.fetch). */
14
+ readonly fetchFn?: FetchLike;
15
+ }
16
+ export declare class EvmJsonRpcClient implements SourceChainRpc, ConstantCaller {
17
+ private readonly rpcUrl;
18
+ private readonly fetchFn;
19
+ constructor(options: EvmJsonRpcClientOptions);
20
+ private call;
21
+ getTransactionInfo(txidHex: string): Promise<SourceTxInfo | null>;
22
+ getNowBlockNumber(): Promise<bigint>;
23
+ constantCall(input: ConstantCallInput): Promise<string>;
24
+ }
25
+ export {};
@@ -0,0 +1,54 @@
1
+ import { selectorHex } from '../contract-call.js';
2
+ function strip0x(h) {
3
+ return h.startsWith('0x') || h.startsWith('0X') ? h.slice(2) : h;
4
+ }
5
+ export class EvmJsonRpcClient {
6
+ rpcUrl;
7
+ fetchFn;
8
+ constructor(options) {
9
+ this.rpcUrl = options.rpcUrl;
10
+ const f = options.fetchFn ?? globalThis.fetch?.bind(globalThis);
11
+ if (!f) {
12
+ throw new Error('No fetch available; pass fetchFn in EvmJsonRpcClientOptions.');
13
+ }
14
+ this.fetchFn = f;
15
+ }
16
+ async call(method, params) {
17
+ const res = await this.fetchFn(this.rpcUrl, {
18
+ method: 'POST',
19
+ headers: { 'content-type': 'application/json' },
20
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
21
+ });
22
+ if (!res.ok) {
23
+ throw new Error(`Ethereum RPC ${method} failed: HTTP ${res.status}`);
24
+ }
25
+ const body = (await res.json());
26
+ if (body.error) {
27
+ throw new Error(`Ethereum RPC ${method} failed: ${body.error.message}`);
28
+ }
29
+ return body.result;
30
+ }
31
+ async getTransactionInfo(txidHex) {
32
+ const receipt = await this.call('eth_getTransactionReceipt', [`0x${strip0x(txidHex)}`]);
33
+ if (!receipt) {
34
+ return null;
35
+ }
36
+ const logs = receipt.logs.map((l) => ({
37
+ address: strip0x(l.address).toLowerCase(),
38
+ topics: l.topics.map((t) => strip0x(t).toLowerCase()),
39
+ data: strip0x(l.data).toLowerCase(),
40
+ }));
41
+ return { blockNumber: BigInt(receipt.blockNumber), success: receipt.status === '0x1', logs };
42
+ }
43
+ async getNowBlockNumber() {
44
+ return BigInt(await this.call('eth_blockNumber', []));
45
+ }
46
+ async constantCall(input) {
47
+ const data = `0x${selectorHex(input.functionSignature)}${strip0x(input.parameterHex ?? '')}`;
48
+ const result = await this.call('eth_call', [
49
+ { from: `0x${strip0x(input.ownerHex)}`, to: `0x${strip0x(input.contractHex)}`, data },
50
+ 'latest',
51
+ ]);
52
+ return strip0x(result);
53
+ }
54
+ }
@@ -0,0 +1,6 @@
1
+ export declare const ETHEREUM_MAINNET_CHAIN_ID = 1;
2
+ export declare const SEPOLIA_CHAIN_ID = 11155111;
3
+ export declare const ETHEREUM_MAINNET_USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
4
+ export declare const ETHEREUM_MAINNET_USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
5
+ export declare const SEPOLIA_USDC = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238";
6
+ export declare const EVM_DEFAULT_CONFIRMATIONS = 64;
@@ -0,0 +1,6 @@
1
+ export const ETHEREUM_MAINNET_CHAIN_ID = 1;
2
+ export const SEPOLIA_CHAIN_ID = 11155111;
3
+ export const ETHEREUM_MAINNET_USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
4
+ export const ETHEREUM_MAINNET_USDT = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
5
+ export const SEPOLIA_USDC = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238';
6
+ export const EVM_DEFAULT_CONFIRMATIONS = 64;
@@ -0,0 +1,3 @@
1
+ import type { ChainFamilyAdapter } from '../family.js';
2
+ export declare function evmChainRef(chainId: number): string;
3
+ export declare const evmFamily: ChainFamilyAdapter;