@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
package/DEMO.md ADDED
@@ -0,0 +1,255 @@
1
+ # Real end-to-end bridge demo — Tron Nile → Unicity
2
+
3
+ This is a **live** demo. It deploys a real lock contract to the **Tron Nile
4
+ testnet**, locks USDT there, mints a **real Unicity token** (testnet2) whose
5
+ mint reason is the on-chain Tron lock, transfers that token to a second owner
6
+ over the Unicity aggregator, and has the receiver re-verify it — which
7
+ re-queries the live Tron node to confirm the lock still backs the token.
8
+
9
+ **Finality model.** The party who locked the asset trusts its own lock, so it
10
+ mints as soon as the lock tx is **in a block** — it does not wait for `K`
11
+ confirmations. The confirmation threshold is a *verifier-side* parameter: an
12
+ independent receiver enforces `K` (default 20) and, if the lock isn't final yet,
13
+ reports "awaiting source finality" and **retries until it is** (see step 5).
14
+
15
+ Nothing here is mocked: every step produces an on-chain transaction or a real
16
+ aggregator commitment you can open in a block explorer.
17
+
18
+ ```
19
+ Tron Nile (TVM) Unicity testnet2 (aggregator)
20
+ ─────────────── ─────────────────────────────
21
+ MockTRC20 (USDT) ──approve──┐
22
+ ▼
23
+ UnicityLock.lock(amount, tokenId, recipientCommitment)
24
+ │ emits Lock(nonce, from, amount, tokenId, recipientCommitment)
25
+ │
26
+ └────────────── mint reason (self-contained proof) ──────────────┐
27
+ ▼
28
+ MintTransaction(justification) ─┴─► Token
29
+ │ (Token.mint re-checks the
30
+ │ lock against live Nile RPC)
31
+ ▼
32
+ transfer ─► new owner
33
+ ▼
34
+ receiver: token.verify() ── re-queries Nile ──► OK
35
+ ```
36
+
37
+ ## What you need
38
+
39
+ | Requirement | How to get it |
40
+ |---|---|
41
+ | Node ≥ 22 | `node -v` |
42
+ | A funded **Tron Nile** account | private key (hex) holding test TRX — faucet: https://nileex.io/join/getJoinPage |
43
+ | (optional) TronGrid API key | https://www.trongrid.io — raises rate limits |
44
+ | Network access to Unicity testnet2 | `https://gateway.testnet2.unicity.network` (default, no key) |
45
+
46
+ The Unicity recipient/owner keys are **generated by the demo** (throwaway). The
47
+ USDT is a `MockTRC20` the demo deploys, so you don't need real Nile USDT.
48
+
49
+ ## Setup
50
+
51
+ ```bash
52
+ cd packages/bridge-plugin
53
+ npm install # or run npm install from the repository root
54
+
55
+ # Required: a Nile-testnet account funded with test TRX.
56
+ export TRON_PRIVATE_KEY=<your-nile-private-key-hex>
57
+
58
+ # Optional overrides (defaults shown):
59
+ export TRON_RPC=https://nile.trongrid.io
60
+ # export TRON_API_KEY=<trongrid-api-key>
61
+ export AMOUNT=1000000 # 1.000000 USDT (6 decimals)
62
+ export CONFIRMATIONS=20 # receiver-side finality threshold (K)
63
+ export MINT_CONFIRMATIONS=0 # minter self-trust: 0 = just needs to be in a block
64
+ # export VERIFY_TIMEOUT_MS=300000 # receiver keeps retrying to finality this long
65
+ # export VERIFY_RETRY_MS=6000 # delay between receiver retries
66
+ export UNICITY_AGGREGATOR=https://gateway.testnet2.unicity.network
67
+ # export UNICITY_API_KEY=<aggregator-api-key> # only if your gateway requires one
68
+ export UNICITY_NETWORK_ID=4
69
+ ```
70
+
71
+ State is written to `demo/.demo-state.json` between steps, so you can run each
72
+ command independently and inspect what happened. Delete that file to start over.
73
+
74
+ ---
75
+
76
+ ## Step 1 — Deploy the lock + test USDT to Tron Nile
77
+
78
+ ```bash
79
+ npm run e2e deploy
80
+ ```
81
+
82
+ Deploys `MockTRC20` (the bridged "USDT") and `UnicityLock(asset, admin)`, then
83
+ mints test-USDT to your account. Prints the two contract addresses, the deploy
84
+ tx hashes (open them on https://nile.tronscan.org), and the **deterministic
85
+ bridged-asset identifiers** (`TokenType`, `coinId`) derived from
86
+ `chainId + assetContract`.
87
+
88
+ ```
89
+ Deployer: T... (chainId 3448148188)
90
+ Deploying MockTRC20 (USDT stand-in)...
91
+ asset (USDT) = T... https://nile.tronscan.org/#/transaction/...
92
+ Deploying UnicityLock(asset, admin)...
93
+ lock = T... https://nile.tronscan.org/#/transaction/...
94
+ Minting 10000000 test-USDT to deployer...
95
+ ✔ deploy complete.
96
+ ```
97
+
98
+ ### Step 1 alternative — attach to the deployed vault and real Nile USDT
99
+
100
+ ```bash
101
+ TRON_VAULT=TTKKLyhnRRQ7XV5vsRarV8xWWEvF9225mY \
102
+ TRON_USDT=TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf \
103
+ npm run e2e attach
104
+ ```
105
+
106
+ Skips deployment and records the live Nile vault (`deployments/nile/nile-usdt.json`)
107
+ and the faucet USDT as the target. Your `TRON_PRIVATE_KEY` account must already
108
+ hold Nile USDT (faucet: https://nileex.io/join/getJoinPage) and TRX for fees.
109
+ Everything after (`lock`, `mint`, `transfer`, `verify`) is unchanged, and the
110
+ resulting token is minted against the same vault the wallet manifest points at.
111
+
112
+ ## Step 2 — Lock USDT, bound to a specific Unicity token + recipient
113
+
114
+ ```bash
115
+ npm run e2e lock
116
+ ```
117
+
118
+ Generates the Unicity recipient key, derives the `tokenId` from a fresh salt,
119
+ and computes `recipientCommitment = SHA256(recipient predicate)`. Then on Tron
120
+ it `approve`s and calls `lock(amount, tokenId, recipientCommitment)`. The
121
+ `Lock` event commits the deposit to **exactly this token id and recipient** — so
122
+ it can fund one token, ownable only by that recipient.
123
+
124
+ ```
125
+ Bridge intent:
126
+ Unicity tokenId = ...
127
+ recipientCommitment = ...
128
+ amount = 1000000 (6 decimals)
129
+ approve tx = https://nile.tronscan.org/#/transaction/...
130
+ lock tx = https://nile.tronscan.org/#/transaction/...
131
+ block = ..., logIndex = 0, nonce = 0
132
+ ✔ lock complete.
133
+ ```
134
+
135
+ ## Step 3 — Mint the bridged Unicity token (immediately, no finality wait)
136
+
137
+ ```bash
138
+ npm run e2e mint
139
+ ```
140
+
141
+ Builds the mint reason — a self-contained CBOR proof (`tag 1330002`) referencing
142
+ the real Nile lock tx — and mints the token on Unicity testnet2. `Token.mint`
143
+ runs the bridge plugin's verifier at `MINT_CONFIRMATIONS` (default **0**), so it
144
+ confirms the lock **exists, succeeded, and binds this exact token id +
145
+ recipient** — without waiting for finality, because the minter is the locker and
146
+ trusts its own deposit. (Run this right after `lock`; you don't wait a minute.)
147
+
148
+ ```
149
+ Submitting mint commitment to https://gateway.testnet2.unicity.network ...
150
+ Waiting for inclusion proof + checking the lock is in a block on Nile...
151
+ ✔ mint complete. A real Unicity token now encapsulates the bridged USDT.
152
+ tokenId = ...
153
+ value = 1000000 (coinId ...)
154
+ ```
155
+
156
+ > Optional: `npm run e2e wait` is a read-only helper that just watches
157
+ > confirmations accrue on Nile. It's not on the critical path.
158
+
159
+ ## Step 4 — Transfer the token to a second owner
160
+
161
+ ```bash
162
+ npm run e2e transfer
163
+ ```
164
+
165
+ Generates a new owner, builds a `TransferTransaction`, certifies it on the
166
+ aggregator, and applies it. This is an ordinary Unicity off-chain transfer of
167
+ the bridged token.
168
+
169
+ ## Step 5 — Receiver verifies the received token (enforces finality)
170
+
171
+ ```bash
172
+ npm run e2e verify
173
+ ```
174
+
175
+ The receiver deserializes the token and calls `token.verify(...)`, which checks
176
+ ownership + history **and re-runs the bridge mint-reason check against the live
177
+ Tron node** at the strict `CONFIRMATIONS` threshold. If you ran `mint`
178
+ immediately, the lock may not be final yet — the receiver then reports
179
+ "awaiting source finality (X < K)" and **retries every few seconds until it is**,
180
+ rather than failing:
181
+
182
+ ```
183
+ Receiver verifies from scratch (requires 20 confirmations on Tron)...
184
+ [attempt 1] Insufficient confirmations: 4 < 20 (awaiting source finality).
185
+ not final yet — retrying in 6s ...
186
+ [attempt 2] Insufficient confirmations: 11 < 20 (awaiting source finality).
187
+ not final yet — retrying in 6s ...
188
+
189
+ ownership + history (Unicity) : OK
190
+ mint reason (re-checked vs Nile): OK
191
+ ✔ RECEIVED TOKEN VERIFIED.
192
+ The token is genuinely backed by 1000000 locked USDT on Tron Nile,
193
+ bound to this token id, and now owned by the receiver — no trusted bridge operator.
194
+ ```
195
+
196
+ Exit codes: `0` verified, `1` genuine rejection (the token does **not** back its
197
+ claimed lock — full trace printed), `2` timed out still awaiting finality (re-run
198
+ later, or lower `CONFIRMATIONS` as a receiver-side trust choice). A real
199
+ rejection (e.g. wrong amount, wrong recipient) is reported as fatal immediately —
200
+ only insufficient-confirmations is treated as transient and retried.
201
+
202
+ ---
203
+
204
+ ## One-shot
205
+
206
+ To run the whole flow end to end (with the finality wait built in):
207
+
208
+ ```bash
209
+ TRON_PRIVATE_KEY=<hex> ./demo/run-demo.sh
210
+ ```
211
+
212
+ ## Inspect / reset
213
+
214
+ ```bash
215
+ npm run e2e status # what's been done so far
216
+ cat demo/.demo-state.json # addresses, tx hashes, tokenId, serialized token
217
+ rm demo/.demo-state.json # start over
218
+ ```
219
+
220
+ ## How this maps to the design
221
+
222
+ - **Mint reason format & verification rule:** [`../../docs/spec/MINT_REASON.md`](../../docs/spec/MINT_REASON.md)
223
+ - **Lock contract:** [`../../contracts/tron/contracts/UnicityLock.sol`](../../contracts/tron/contracts/UnicityLock.sol)
224
+ - **Verifier (the security):** [`src/TronUsdtMintJustificationVerifier.ts`](src/TronUsdtMintJustificationVerifier.ts)
225
+ - **Offline security demo (mock RPC, attack matrix):** `npm run cli demo`
226
+
227
+ ## Bridge-BACK demo (return path, live testnet2)
228
+
229
+ `npm run e2e:back` runs the return path against the **live** Unicity aggregator
230
+ (only the Tron lock is mocked in-process): it mints a **bridge-lock-genesis**
231
+ token (real `TronUsdtLockJustification`, structurally verified at mint), burns it
232
+ to a canonical `BridgeBackReason` — terminal recipient predicate
233
+ `BurnPredicate(H(reasonBytes))` with `reasonBytes` in the burn's auxiliary data
234
+ (00 §4) — certifies the burn on the aggregator, then derives the `nullifier` +
235
+ return leaf from the **real** certified burn (state id + tx hash) exactly as the
236
+ prover will (00 §5/§7). It writes the burned-token blob + prover config +
237
+ witness-request envelope to `demo/.bridge-back-state.json`. Reads the repo-root
238
+ `.env` (`UNICITY_GATEWAY`, `UNICITY_API_KEY`, `UNICITY_TRUSTBASE`).
239
+ Source: [`demo/bridge-back-e2e.ts`](demo/bridge-back-e2e.ts).
240
+
241
+ **Cross-stack closure.** The Rust prover then decodes that exact live blob and
242
+ confirms it derives identical values (E3 `bridge_lock_obligation` lockDigest +
243
+ nonce, `configHash`, burn state id/tx hash, `burnTransitionId`, `nullifier`):
244
+
245
+ ```
246
+ cd ../prover && cargo run -p bridge-return-host --example cross_check_live -- \
247
+ ../packages/bridge-plugin/demo/.bridge-back-state.json
248
+ ```
249
+
250
+ > Interop note: the token's value payload is the **wallet's format**
251
+ > (`SpherePaymentData`, CBOR tag 39050; `protocol/interop.md` §2.1), written and
252
+ > read by `encodeBridgePaymentData` / `decodeBridgePaymentData` in `src/value.ts`.
253
+ > The bespoke `encodeBridgedValue` there is CLI-only. The Rust prover's decoder
254
+ > follows in the SDK-v3 port (`docs/dev-plan/09-testnet-e2e.md`, Phase 2); until
255
+ > then `cross_check_live` does not accept tokens minted by this demo.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @unicitylabs/bridge-plugin
2
+
3
+ Unicity bridge plugin that validates **bridged USDT-on-Tron** tokens. It is an
4
+ `IMintJustificationVerifier` for the Unicity state-transition SDK: every
5
+ recipient of a bridged token re-checks the token's **mint reason** (a
6
+ self-contained Tron lock proof) against a Tron RPC node. There is no trusted
7
+ bridge operator.
8
+
9
+ See the design docs:
10
+ - [`../../docs/spec/MINT_REASON.md`](../../docs/spec/MINT_REASON.md) — proof format + verification rule
11
+ - [`../../docs/spec/PLUGIN_ARCHITECTURE.md`](../../docs/spec/PLUGIN_ARCHITECTURE.md) — how plugins plug in
12
+ - [`../../docs/spec/ZK_BACK3.md`](../../docs/spec/ZK_BACK3.md) — returning to Tron
13
+
14
+ ## How security works
15
+
16
+ Minting on Unicity is permissionless (the minter key is derived from the
17
+ `tokenId`), so all bridge security comes from this verifier. The Tron
18
+ `UnicityLock` contract commits each deposit to the exact Unicity `tokenId` and to
19
+ `recipientCommitment = SHA256(recipient predicate)`. A deposit can therefore fund
20
+ exactly one token, owned only by the designated recipient:
21
+
22
+ | Attack | Rejected because |
23
+ |---|---|
24
+ | Mint without a real lock | RPC finds no matching Lock event |
25
+ | Replay a lock for a second token | event.unicityTokenId ≠ this token's id |
26
+ | Inflate value vs. locked amount | token value ≠ event amount |
27
+ | Steal/front-run a lock | event.recipientCommitment ≠ H(recipient) |
28
+ | Point at a rogue lock contract | trust-anchor (chain/contract/asset) mismatch |
29
+ | Use an unconfirmed lock | confirmations < threshold |
30
+
31
+ ## Usage
32
+
33
+ ```ts
34
+ import { createTronUsdtBridgePlugin, tronMainnetUsdtConfig } from '@unicitylabs/bridge-plugin';
35
+ import { MintJustificationVerifierService } from '@unicitylabs/state-transition-sdk/lib/transaction/verification/MintJustificationVerifierService.js';
36
+
37
+ const plugin = createTronUsdtBridgePlugin(
38
+ tronMainnetUsdtConfig('TYourDeployedUnicityLockAddress'),
39
+ // { rpc, extractAmount } // optional: inject a TronRpc and a value extractor
40
+ );
41
+
42
+ const service = new MintJustificationVerifierService();
43
+ service.register(plugin.verifier); // dispatched by CBOR tag 1330002
44
+
45
+ // plugin.tokenTypeHex / plugin.coinIdHex identify the bridged asset.
46
+ ```
47
+
48
+ In a Sphere wallet the plugin is registered through the SDK's generic token-plugin
49
+ seam, `Sphere.init({ plugins: [bridgeTokenPlugin(loaded)] })` (see
50
+ `src/wallet/token-plugin.ts`); the wallet SDK has no bridge-specific code. The
51
+ bridge-in mint and the bridge-out burn are composed over the wallet's generic
52
+ `mintCustom` / `burn` by `@unicitylabs/bridge-core` (`mintBridgedToken`,
53
+ `burnForReturn`, `recoverPendingBurns`). The token's declared value is checked
54
+ by `decodeBridgePaymentData` (the wallet's value format, `src/value.ts`).
55
+
56
+ ## CLI
57
+
58
+ ```bash
59
+ npm run cli demo # offline security demo (mock Tron RPC)
60
+ # or after build:
61
+ node lib/cli/main.js demo
62
+
63
+ # verify a real serialized CertifiedMintTransaction against a live node:
64
+ node lib/cli/main.js verify --token <hex> --lock <addr> --asset <addr> \
65
+ --chain mainnet --rpc https://api.trongrid.io [--api-key <key>]
66
+ ```
67
+
68
+ The `demo` exits non-zero if the valid token is rejected or any attack is
69
+ accepted. Sample output:
70
+
71
+ ```
72
+ ✔ [OK ] Valid bridged mint (lock finalized, bound to this token+recipient)
73
+ ✔ [FAIL] Attack: inflate token value above locked amount
74
+ ✔ [FAIL] Attack: tamper justification amount
75
+ ✔ [FAIL] Attack: replay lock for a different token id
76
+ ✔ [FAIL] Attack: steal lock by swapping recipient
77
+ ✔ [FAIL] Attack: forged lock contract emits the event
78
+ ✔ [FAIL] Attack: spend lock before finality (insufficient confirmations)
79
+ All checks behaved as expected: valid token accepted, every attack rejected.
80
+ ```
81
+
82
+ ## Develop
83
+
84
+ ```bash
85
+ npm install # from the repository root; links local workspaces
86
+ npm run typecheck
87
+ npm test # node:test via tsx
88
+ npm run build
89
+ ```
@@ -0,0 +1,31 @@
1
+ /**
2
+ * CBOR tag of the bridge lock justification, the same on every source chain:
3
+ * the chain id inside the justification tells the families apart, and the
4
+ * wallet's verifier for this tag dispatches on it.
5
+ */
6
+ export declare const BRIDGE_LOCK_JUSTIFICATION_TAG = 1330002n;
7
+ /** Decoded contents of a lock justification (the token's mint reason). */
8
+ export interface BridgeLockJustificationData {
9
+ /** Source chain id (Tron mainnet 728126428, Nile 3448148188, Sepolia 11155111). */
10
+ readonly chainId: number;
11
+ /** 20-byte address of the canonical vault (lock) contract. */
12
+ readonly lockContract: Uint8Array;
13
+ /** 20-byte address of the bridged asset's token contract. */
14
+ readonly assetContract: Uint8Array;
15
+ /** 32-byte transaction hash of the lock() call. */
16
+ readonly txid: Uint8Array;
17
+ /** Index of the Lock event within that transaction's logs. */
18
+ readonly logIndex: number;
19
+ /** Locked amount in the asset's smallest unit. */
20
+ readonly amount: bigint;
21
+ /** Lock nonce assigned by the contract (echoed in the Lock event). */
22
+ readonly nonce: bigint;
23
+ }
24
+ /** Encodes/decodes the self-contained lock proof carried in a token's mint reason. */
25
+ export declare class BridgeLockJustification {
26
+ readonly data: BridgeLockJustificationData;
27
+ static readonly CBOR_TAG = 1330002n;
28
+ constructor(data: BridgeLockJustificationData);
29
+ toCBOR(): Uint8Array;
30
+ static fromCBOR(bytes: Uint8Array): BridgeLockJustification;
31
+ }
@@ -0,0 +1,41 @@
1
+ import { CborDeserializer } from '@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborDeserializer.js';
2
+ import { CborSerializer } from '@unicitylabs/state-transition-sdk/lib/serialization/cbor/CborSerializer.js';
3
+ /**
4
+ * CBOR tag of the bridge lock justification, the same on every source chain:
5
+ * the chain id inside the justification tells the families apart, and the
6
+ * wallet's verifier for this tag dispatches on it.
7
+ */
8
+ export const BRIDGE_LOCK_JUSTIFICATION_TAG = 1330002n;
9
+ const VERSION = 1n;
10
+ /** Encodes/decodes the self-contained lock proof carried in a token's mint reason. */
11
+ export class BridgeLockJustification {
12
+ data;
13
+ static CBOR_TAG = BRIDGE_LOCK_JUSTIFICATION_TAG;
14
+ constructor(data) {
15
+ this.data = data;
16
+ }
17
+ toCBOR() {
18
+ const d = this.data;
19
+ return CborSerializer.encodeTag(BridgeLockJustification.CBOR_TAG, CborSerializer.encodeArray(CborSerializer.encodeUnsignedInteger(VERSION), CborSerializer.encodeUnsignedInteger(d.chainId), CborSerializer.encodeByteString(d.lockContract), CborSerializer.encodeByteString(d.assetContract), CborSerializer.encodeByteString(d.txid), CborSerializer.encodeUnsignedInteger(d.logIndex), CborSerializer.encodeUnsignedInteger(d.amount), CborSerializer.encodeUnsignedInteger(d.nonce)));
20
+ }
21
+ static fromCBOR(bytes) {
22
+ const tag = CborDeserializer.decodeTag(bytes);
23
+ if (tag.tag !== BridgeLockJustification.CBOR_TAG) {
24
+ throw new Error(`Invalid CBOR tag for BridgeLockJustification: ${tag.tag}`);
25
+ }
26
+ const items = CborDeserializer.decodeArray(tag.data, 8);
27
+ const version = CborDeserializer.decodeUnsignedInteger(items[0]);
28
+ if (version !== VERSION) {
29
+ throw new Error(`Unsupported BridgeLockJustification version: ${version}`);
30
+ }
31
+ return new BridgeLockJustification({
32
+ chainId: Number(CborDeserializer.decodeUnsignedInteger(items[1])),
33
+ lockContract: CborDeserializer.decodeByteString(items[2]),
34
+ assetContract: CborDeserializer.decodeByteString(items[3]),
35
+ txid: CborDeserializer.decodeByteString(items[4]),
36
+ logIndex: Number(CborDeserializer.decodeUnsignedInteger(items[5])),
37
+ amount: CborDeserializer.decodeUnsignedInteger(items[6]),
38
+ nonce: CborDeserializer.decodeUnsignedInteger(items[7]),
39
+ });
40
+ }
41
+ }
@@ -0,0 +1,12 @@
1
+ import type { CertifiedMintTransaction } from '@unicitylabs/state-transition-sdk/lib/transaction/CertifiedMintTransaction.js';
2
+ import type { Token } from '@unicitylabs/state-transition-sdk/lib/transaction/Token.js';
3
+ import type { IMintJustificationVerifier } from '@unicitylabs/state-transition-sdk/lib/transaction/verification/IMintJustificationVerifier.js';
4
+ import { VerificationResult } from '@unicitylabs/state-transition-sdk/lib/verification/VerificationResult.js';
5
+ import { VerificationStatus } from '@unicitylabs/state-transition-sdk/lib/verification/VerificationStatus.js';
6
+ import type { LockMintJustificationVerifier } from './LockMintJustificationVerifier.js';
7
+ export declare class BridgeMintJustificationVerifier implements IMintJustificationVerifier {
8
+ private readonly verifiers;
9
+ constructor(verifiers: readonly LockMintJustificationVerifier[]);
10
+ get tag(): bigint;
11
+ verify(transaction: CertifiedMintTransaction, nestedTokenCollector: (token: Token) => void): Promise<VerificationResult<VerificationStatus>>;
12
+ }
@@ -0,0 +1,35 @@
1
+ import { VerificationResult } from '@unicitylabs/state-transition-sdk/lib/verification/VerificationResult.js';
2
+ import { VerificationStatus } from '@unicitylabs/state-transition-sdk/lib/verification/VerificationStatus.js';
3
+ import { BridgeLockJustification, BRIDGE_LOCK_JUSTIFICATION_TAG } from './BridgeLockJustification.js';
4
+ import { toHex } from './hex.js';
5
+ const RULE = 'BridgeMintJustificationVerifier';
6
+ export class BridgeMintJustificationVerifier {
7
+ verifiers;
8
+ constructor(verifiers) {
9
+ this.verifiers = verifiers;
10
+ }
11
+ get tag() {
12
+ return BRIDGE_LOCK_JUSTIFICATION_TAG;
13
+ }
14
+ verify(transaction, nestedTokenCollector) {
15
+ const bytes = transaction.justification;
16
+ if (!bytes) {
17
+ return fail('Transaction has no justification.');
18
+ }
19
+ let j;
20
+ try {
21
+ j = BridgeLockJustification.fromCBOR(bytes);
22
+ }
23
+ catch (e) {
24
+ return fail(`Malformed justification: ${e.message}`);
25
+ }
26
+ const owner = this.verifiers.find((v) => v.accepts(j.data));
27
+ if (!owner) {
28
+ return fail(`No bridge verifies chain ${j.data.chainId} vault 0x${toHex(j.data.lockContract)}.`);
29
+ }
30
+ return owner.verify(transaction, nestedTokenCollector);
31
+ }
32
+ }
33
+ function fail(message) {
34
+ return Promise.resolve(new VerificationResult(RULE, VerificationStatus.FAIL, message));
35
+ }
@@ -0,0 +1,42 @@
1
+ import type { CertifiedMintTransaction } from '@unicitylabs/state-transition-sdk/lib/transaction/CertifiedMintTransaction.js';
2
+ import type { Token } from '@unicitylabs/state-transition-sdk/lib/transaction/Token.js';
3
+ import type { IMintJustificationVerifier } from '@unicitylabs/state-transition-sdk/lib/transaction/verification/IMintJustificationVerifier.js';
4
+ import { VerificationResult } from '@unicitylabs/state-transition-sdk/lib/verification/VerificationResult.js';
5
+ import { VerificationStatus } from '@unicitylabs/state-transition-sdk/lib/verification/VerificationStatus.js';
6
+ import type { ChainFamily } from '@unicitylabs/bridge-core';
7
+ import { type BridgeLockJustificationData } from './BridgeLockJustification.js';
8
+ import type { SourceChainRpc } from './source-chain.js';
9
+ import { type BridgedAmountExtractor } from './value.js';
10
+ /** Trust anchors + identifiers a verifier instance is bound to. */
11
+ export interface ResolvedBridgeConfig {
12
+ readonly family: ChainFamily;
13
+ readonly chainId: number;
14
+ /** 20-byte EVM-form address, lowercase hex. */
15
+ readonly lockContractHex: string;
16
+ /** 20-byte EVM-form address, lowercase hex. */
17
+ readonly assetContractHex: string;
18
+ readonly confirmations: number;
19
+ /** 32-byte Unicity TokenType for this asset. */
20
+ readonly tokenType: Uint8Array;
21
+ /** 32-byte Sphere coinId for this asset. */
22
+ readonly coinId: Uint8Array;
23
+ }
24
+ export interface LockVerifierDeps {
25
+ readonly rpc: SourceChainRpc;
26
+ /** Defaults to the simple CLI value envelope decoder. */
27
+ readonly extractAmount?: BridgedAmountExtractor;
28
+ }
29
+ /**
30
+ * Validates a bridged token by re-checking its mint reason against a node of
31
+ * the source chain: the lock exists, is final, has the right amount, and
32
+ * commits to exactly this token's id + recipient. See docs/spec/MINT_REASON.md.
33
+ */
34
+ export declare class LockMintJustificationVerifier implements IMintJustificationVerifier {
35
+ private readonly config;
36
+ private readonly rpc;
37
+ private readonly extractAmount;
38
+ constructor(config: ResolvedBridgeConfig, deps: LockVerifierDeps);
39
+ get tag(): bigint;
40
+ accepts(j: BridgeLockJustificationData): boolean;
41
+ verify(transaction: CertifiedMintTransaction, _nestedTokenCollector: (token: Token) => void): Promise<VerificationResult<VerificationStatus>>;
42
+ }
@@ -0,0 +1,108 @@
1
+ import { VerificationResult } from '@unicitylabs/state-transition-sdk/lib/verification/VerificationResult.js';
2
+ import { VerificationStatus } from '@unicitylabs/state-transition-sdk/lib/verification/VerificationStatus.js';
3
+ import { bytesEqual, toHex } from './hex.js';
4
+ import { recipientCommitment } from './identifiers.js';
5
+ import { decodeLockEvent } from './lock-event.js';
6
+ import { BridgeLockJustification, BRIDGE_LOCK_JUSTIFICATION_TAG } from './BridgeLockJustification.js';
7
+ import { decodeBridgePaymentData } from './value.js';
8
+ const RULE = 'LockMintJustificationVerifier';
9
+ function fail(message) {
10
+ return new VerificationResult(RULE, VerificationStatus.FAIL, message);
11
+ }
12
+ /**
13
+ * Validates a bridged token by re-checking its mint reason against a node of
14
+ * the source chain: the lock exists, is final, has the right amount, and
15
+ * commits to exactly this token's id + recipient. See docs/spec/MINT_REASON.md.
16
+ */
17
+ export class LockMintJustificationVerifier {
18
+ config;
19
+ rpc;
20
+ extractAmount;
21
+ constructor(config, deps) {
22
+ this.config = config;
23
+ this.rpc = deps.rpc;
24
+ this.extractAmount = deps.extractAmount ?? decodeBridgePaymentData;
25
+ }
26
+ get tag() {
27
+ return BRIDGE_LOCK_JUSTIFICATION_TAG;
28
+ }
29
+ accepts(j) {
30
+ return j.chainId === this.config.chainId && toHex(j.lockContract).toLowerCase() === this.config.lockContractHex;
31
+ }
32
+ async verify(transaction, _nestedTokenCollector) {
33
+ const bytes = transaction.justification;
34
+ if (!bytes) {
35
+ return fail('Transaction has no justification.');
36
+ }
37
+ let justification;
38
+ try {
39
+ justification = BridgeLockJustification.fromCBOR(bytes);
40
+ }
41
+ catch (e) {
42
+ return fail(`Malformed justification: ${e.message}`);
43
+ }
44
+ const j = justification.data;
45
+ // 1. Trust anchors: which chain/contract/asset is authoritative.
46
+ if (j.chainId !== this.config.chainId) {
47
+ return fail(`Chain id mismatch: proof ${j.chainId}, expected ${this.config.chainId}.`);
48
+ }
49
+ if (toHex(j.lockContract).toLowerCase() !== this.config.lockContractHex) {
50
+ return fail('Lock contract is not the canonical bridge contract.');
51
+ }
52
+ if (toHex(j.assetContract).toLowerCase() !== this.config.assetContractHex) {
53
+ return fail('Asset contract is not the canonical bridged asset.');
54
+ }
55
+ // 2. Token type must be this asset's type.
56
+ if (!bytesEqual(transaction.tokenType.bytes, this.config.tokenType)) {
57
+ return fail('Token type does not match this bridged asset.');
58
+ }
59
+ // 3-4. Fetch the lock tx and require success + finality.
60
+ const txInfo = await this.rpc.getTransactionInfo(toHex(j.txid));
61
+ if (!txInfo) {
62
+ return fail(`Lock transaction not found: ${toHex(j.txid)}.`);
63
+ }
64
+ if (!txInfo.success) {
65
+ return fail('Lock transaction did not succeed.');
66
+ }
67
+ const tip = await this.rpc.getNowBlockNumber();
68
+ const confirmations = tip - txInfo.blockNumber;
69
+ if (confirmations < BigInt(this.config.confirmations)) {
70
+ return fail(`Insufficient confirmations: ${confirmations} < ${this.config.confirmations} (awaiting source finality).`);
71
+ }
72
+ // 5. Locate the Lock event and confirm it came from the canonical contract.
73
+ const log = txInfo.logs[j.logIndex];
74
+ if (!log) {
75
+ return fail(`No log at index ${j.logIndex}.`);
76
+ }
77
+ if (log.address.toLowerCase() !== this.config.lockContractHex) {
78
+ return fail('Log was not emitted by the canonical lock contract.');
79
+ }
80
+ const event = decodeLockEvent(log);
81
+ if (!event) {
82
+ return fail(`Log at index ${j.logIndex} is not a Lock event.`);
83
+ }
84
+ // 6. Amount: event == justification == token's declared value.
85
+ if (event.amount !== j.amount) {
86
+ return fail(`Amount mismatch: event ${event.amount}, justification ${j.amount}.`);
87
+ }
88
+ const declared = this.extractAmount(transaction.data, this.config.coinId);
89
+ if (declared == null) {
90
+ return fail('Token declares no bridged-asset value.');
91
+ }
92
+ if (declared !== event.amount) {
93
+ return fail(`Token value ${declared} does not match locked amount ${event.amount}.`);
94
+ }
95
+ // 7. Binding: the lock commits to exactly this token id, recipient and nonce.
96
+ if (event.nonce !== j.nonce) {
97
+ return fail(`Nonce mismatch: event ${event.nonce}, justification ${j.nonce}.`);
98
+ }
99
+ if (!bytesEqual(event.unicityTokenId, transaction.tokenId.bytes)) {
100
+ return fail('Lock is bound to a different token id (replay/forgery).');
101
+ }
102
+ const expectedRecipient = recipientCommitment(transaction.recipient.toCBOR());
103
+ if (!bytesEqual(event.recipientCommitment, expectedRecipient)) {
104
+ return fail('Lock is bound to a different recipient (theft/front-run).');
105
+ }
106
+ return new VerificationResult(RULE, VerificationStatus.OK);
107
+ }
108
+ }
@@ -0,0 +1,13 @@
1
+ /** Tron mainnet address prefix byte (0x41). */
2
+ export declare const TRON_ADDRESS_PREFIX = 65;
3
+ /**
4
+ * Normalize any source-chain address form to the 20-byte EVM-style address as
5
+ * lowercase hex (no `0x`, no `41` prefix), the form event logs use for `address`
6
+ * on both families.
7
+ *
8
+ * Accepts:
9
+ * - Tron base58check `T...` addresses,
10
+ * - Tron `41`-prefixed 21-byte hex (TronWeb `toHex()` form),
11
+ * - bare 20-byte hex, optionally `0x`-prefixed (Ethereum, or either family's raw form).
12
+ */
13
+ export declare function toEvmAddressHex(address: string): string;