@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.
- package/DEMO.md +255 -0
- package/README.md +89 -0
- package/lib/BridgeLockJustification.d.ts +31 -0
- package/lib/BridgeLockJustification.js +41 -0
- package/lib/BridgeMintJustificationVerifier.d.ts +12 -0
- package/lib/BridgeMintJustificationVerifier.js +35 -0
- package/lib/LockMintJustificationVerifier.d.ts +42 -0
- package/lib/LockMintJustificationVerifier.js +108 -0
- package/lib/address.d.ts +13 -0
- package/lib/address.js +71 -0
- package/lib/bridge-back/abi.d.ts +30 -0
- package/lib/bridge-back/abi.js +89 -0
- package/lib/bridge-back/burn.d.ts +79 -0
- package/lib/bridge-back/burn.js +78 -0
- package/lib/bridge-back/burned-blob.d.ts +6 -0
- package/lib/bridge-back/burned-blob.js +16 -0
- package/lib/bridge-back/cbor.d.ts +32 -0
- package/lib/bridge-back/cbor.js +146 -0
- package/lib/bridge-back/derivations.d.ts +112 -0
- package/lib/bridge-back/derivations.js +201 -0
- package/lib/bridge-back/index.d.ts +11 -0
- package/lib/bridge-back/index.js +11 -0
- package/lib/cli/main.d.ts +2 -0
- package/lib/cli/main.js +149 -0
- package/lib/cli/scenario.d.ts +36 -0
- package/lib/cli/scenario.js +94 -0
- package/lib/config.d.ts +25 -0
- package/lib/config.js +1 -0
- package/lib/contract-call.d.ts +13 -0
- package/lib/contract-call.js +19 -0
- package/lib/evm/EvmRpcClient.d.ts +25 -0
- package/lib/evm/EvmRpcClient.js +54 -0
- package/lib/evm/config.d.ts +6 -0
- package/lib/evm/config.js +6 -0
- package/lib/evm/family.d.ts +3 -0
- package/lib/evm/family.js +21 -0
- package/lib/evm/index.d.ts +6 -0
- package/lib/evm/index.js +6 -0
- package/lib/evm/presentation.d.ts +4 -0
- package/lib/evm/presentation.js +21 -0
- package/lib/evm/providers.d.ts +3 -0
- package/lib/evm/providers.js +9 -0
- package/lib/evm/signer.d.ts +43 -0
- package/lib/evm/signer.js +91 -0
- package/lib/families.d.ts +4 -0
- package/lib/families.js +11 -0
- package/lib/family.d.ts +16 -0
- package/lib/family.js +1 -0
- package/lib/hex.d.ts +4 -0
- package/lib/hex.js +33 -0
- package/lib/identifiers.d.ts +14 -0
- package/lib/identifiers.js +22 -0
- package/lib/index.d.ts +48 -0
- package/lib/index.js +55 -0
- package/lib/lock-event.d.ts +28 -0
- package/lib/lock-event.js +48 -0
- package/lib/source-chain.d.ts +29 -0
- package/lib/source-chain.js +1 -0
- package/lib/tron/TronRpcClient.d.ts +39 -0
- package/lib/tron/TronRpcClient.js +100 -0
- package/lib/tron/config.d.ts +11 -0
- package/lib/tron/config.js +31 -0
- package/lib/tron/family.d.ts +4 -0
- package/lib/tron/family.js +18 -0
- package/lib/tron/presentation.d.ts +10 -0
- package/lib/tron/presentation.js +18 -0
- package/lib/tron/providers.d.ts +46 -0
- package/lib/tron/providers.js +32 -0
- package/lib/tron/signer.d.ts +166 -0
- package/lib/tron/signer.js +238 -0
- package/lib/value.d.ts +23 -0
- package/lib/value.js +58 -0
- package/lib/wallet/allowance.d.ts +20 -0
- package/lib/wallet/allowance.js +27 -0
- package/lib/wallet/backing.d.ts +2 -0
- package/lib/wallet/backing.js +15 -0
- package/lib/wallet/facade.d.ts +97 -0
- package/lib/wallet/facade.js +100 -0
- package/lib/wallet/finality.d.ts +8 -0
- package/lib/wallet/finality.js +28 -0
- package/lib/wallet/index.d.ts +25 -0
- package/lib/wallet/index.js +25 -0
- package/lib/wallet/manifest.d.ts +69 -0
- package/lib/wallet/manifest.js +81 -0
- package/lib/wallet/manifests.d.ts +31 -0
- package/lib/wallet/manifests.js +70 -0
- package/lib/wallet/payout.d.ts +5 -0
- package/lib/wallet/payout.js +14 -0
- package/lib/wallet/registry.d.ts +40 -0
- package/lib/wallet/registry.js +34 -0
- package/lib/wallet/return-client.d.ts +91 -0
- package/lib/wallet/return-client.js +100 -0
- package/lib/wallet/self-mint-verifier.d.ts +10 -0
- package/lib/wallet/self-mint-verifier.js +5 -0
- package/lib/wallet/signer.d.ts +18 -0
- package/lib/wallet/signer.js +1 -0
- package/lib/wallet/source-adapter.d.ts +33 -0
- package/lib/wallet/source-adapter.js +99 -0
- package/lib/wallet/token-plugin.d.ts +4 -0
- package/lib/wallet/token-plugin.js +20 -0
- package/manifests/bridges.nile.json +23 -0
- package/package.json +71 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const DEFAULT_FEE_LIMIT_SUN = 150_000_000; // 150 TRX — generous cap; Nile calls cost far less.
|
|
2
|
+
const SIGN_TIMEOUT_MS = 120_000;
|
|
3
|
+
const BROADCAST_TIMEOUT_MS = 45_000;
|
|
4
|
+
/**
|
|
5
|
+
* Tron's chainId (as used by the bridge config, e.g. Nile `3448148188`) is the
|
|
6
|
+
* low 4 bytes of the genesis block's `blockID`. Deriving it from the wallet's
|
|
7
|
+
* connected node is a real network identity — not the manifest echoing itself.
|
|
8
|
+
*/
|
|
9
|
+
export function chainIdFromGenesisBlockId(blockId) {
|
|
10
|
+
const h = blockId.replace(/^0x/i, '');
|
|
11
|
+
if (h.length < 8)
|
|
12
|
+
throw new Error(`Malformed genesis blockID: ${blockId}`);
|
|
13
|
+
return parseInt(h.slice(-8), 16);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* TronLink-backed signer (priority 1). Uses the injected `window.tronWeb` for
|
|
17
|
+
* building + broadcasting; `tron_requestAccounts` prompts the connection once.
|
|
18
|
+
*/
|
|
19
|
+
export class TronLinkSigner {
|
|
20
|
+
win;
|
|
21
|
+
expectedChainId;
|
|
22
|
+
/** True once `connect()` has prompted; `getAddress()` then reads live silently. */
|
|
23
|
+
connected = false;
|
|
24
|
+
constructor(
|
|
25
|
+
/** The injected window (defaults to the global one in a browser). */
|
|
26
|
+
win = globalThis,
|
|
27
|
+
/** Expected Tron chainId (from the manifest) — for the caller's wrong-network message. */
|
|
28
|
+
expectedChainId) {
|
|
29
|
+
this.win = win;
|
|
30
|
+
this.expectedChainId = expectedChainId;
|
|
31
|
+
}
|
|
32
|
+
tronWeb() {
|
|
33
|
+
const tw = this.win.tronWeb;
|
|
34
|
+
if (!tw) {
|
|
35
|
+
throw new Error('TronLink not found. Install the TronLink extension and reload.');
|
|
36
|
+
}
|
|
37
|
+
return tw;
|
|
38
|
+
}
|
|
39
|
+
async connect() {
|
|
40
|
+
// Prompt the connection once if TronLink is present but not yet authorized.
|
|
41
|
+
if (this.win.tronLink) {
|
|
42
|
+
await this.win.tronLink.request({ method: 'tron_requestAccounts' });
|
|
43
|
+
}
|
|
44
|
+
const addr = this.tronWeb().defaultAddress.base58;
|
|
45
|
+
if (!addr) {
|
|
46
|
+
throw new Error('TronLink is locked or no account is selected.');
|
|
47
|
+
}
|
|
48
|
+
this.connected = true;
|
|
49
|
+
return addr;
|
|
50
|
+
}
|
|
51
|
+
async getAddress() {
|
|
52
|
+
// Read the wallet's *current* account live (silent) — no cache, so a mid-flow
|
|
53
|
+
// account switch is visible to the pre-signature guard (08 §1.4). Only
|
|
54
|
+
// `connect()` ever prompts.
|
|
55
|
+
const addr = this.tronWeb().defaultAddress.base58;
|
|
56
|
+
if (addr) {
|
|
57
|
+
this.connected = true;
|
|
58
|
+
return addr;
|
|
59
|
+
}
|
|
60
|
+
if (!this.connected)
|
|
61
|
+
return this.connect();
|
|
62
|
+
throw new Error('TronLink is locked or no account is selected.');
|
|
63
|
+
}
|
|
64
|
+
async getNetwork() {
|
|
65
|
+
// Derive the wallet node's *current* chainId live (no cache) — a network
|
|
66
|
+
// switch changes the injected node, and the guard must see it (08 §1.4).
|
|
67
|
+
const trx = this.tronWeb().trx;
|
|
68
|
+
if (!trx.getBlockByNumber) {
|
|
69
|
+
throw new Error('This Tron wallet cannot report its network (no genesis access); cannot verify the chain.');
|
|
70
|
+
}
|
|
71
|
+
const genesis = await trx.getBlockByNumber(0);
|
|
72
|
+
const blockId = genesis?.blockID;
|
|
73
|
+
if (!blockId) {
|
|
74
|
+
throw new Error('Could not read the wallet node’s genesis block to determine the network.');
|
|
75
|
+
}
|
|
76
|
+
return chainIdFromGenesisBlockId(blockId);
|
|
77
|
+
}
|
|
78
|
+
onChange(cb) {
|
|
79
|
+
const win = this.win;
|
|
80
|
+
if (!win.addEventListener || !win.removeEventListener)
|
|
81
|
+
return () => { };
|
|
82
|
+
const listener = (e) => {
|
|
83
|
+
// TronLink relays wallet state via window 'message' events.
|
|
84
|
+
const action = e?.data?.message?.action;
|
|
85
|
+
if (action === 'accountsChanged' || action === 'setAccount') {
|
|
86
|
+
cb({ kind: 'accountsChanged' });
|
|
87
|
+
}
|
|
88
|
+
else if (action === 'setNode' || action === 'connectWeb' || action === 'chainChanged') {
|
|
89
|
+
cb({ kind: 'chainChanged' });
|
|
90
|
+
}
|
|
91
|
+
else if (action === 'disconnect' || action === 'disconnectWeb') {
|
|
92
|
+
this.connected = false;
|
|
93
|
+
cb({ kind: 'disconnect' });
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
win.addEventListener('message', listener);
|
|
97
|
+
return () => win.removeEventListener?.('message', listener);
|
|
98
|
+
}
|
|
99
|
+
/** The manifest's expected chainId, for building a clear wrong-network message. */
|
|
100
|
+
get expected() {
|
|
101
|
+
return this.expectedChainId;
|
|
102
|
+
}
|
|
103
|
+
async sendCall(call, opts = {}) {
|
|
104
|
+
return sendCallVia(this.tronWeb(), await this.getAddress(), call, opts);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function tronHex(hex) {
|
|
108
|
+
return '41' + hex.replace(/^0x/i, '').toLowerCase();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Build → **sign (via `sign`)** → broadcast a call over any {InjectedTronWeb} (the
|
|
112
|
+
* dApp-broadcast model). The build + broadcast use `tw`; only the signature is the
|
|
113
|
+
* wallet's concern — so this serves both the injected path (`tw.trx.sign`) and
|
|
114
|
+
* adapter/WalletConnect (the adapter's `signTransaction`).
|
|
115
|
+
*/
|
|
116
|
+
export async function sendCallSigned(tw, issuerBase58, call, sign, opts = {}) {
|
|
117
|
+
const built = await tw.transactionBuilder.triggerSmartContract(tronHex(call.contractHex), call.functionSignature, { feeLimit: opts.feeLimitSun ?? DEFAULT_FEE_LIMIT_SUN, callValue: opts.callValueSun ?? 0 }, call.parameters.map((p) => (p.type === 'address' ? { type: p.type, value: tronHex(p.value) } : { type: p.type, value: p.value })), tw.address.toHex(issuerBase58));
|
|
118
|
+
if (built.result && built.result.result === false) {
|
|
119
|
+
throw new Error(`Tron call ${call.functionSignature} could not be built (constant-call reverted).`);
|
|
120
|
+
}
|
|
121
|
+
const signed = normalizeSignedTransaction(await withTimeout(sign(built.transaction), SIGN_TIMEOUT_MS, `Timed out waiting for wallet signature for ${call.functionSignature}.`), call.functionSignature);
|
|
122
|
+
const receipt = await withTimeout(tw.trx.sendRawTransaction(signed), BROADCAST_TIMEOUT_MS, `Timed out broadcasting ${call.functionSignature} to Tron.`);
|
|
123
|
+
// Reject an explicit broadcast failure (SIGERROR, DUP_TRANSACTION, …) instead
|
|
124
|
+
// of returning a phantom txid the caller would then wait on forever (08 §1.4).
|
|
125
|
+
if (receipt.result === false) {
|
|
126
|
+
throw new Error(`Tron broadcast of ${call.functionSignature} was rejected: ${JSON.stringify(receipt)}`);
|
|
127
|
+
}
|
|
128
|
+
const txid = receipt.txid ?? receipt.transaction?.txID;
|
|
129
|
+
if (!txid) {
|
|
130
|
+
throw new Error(`Tron broadcast of ${call.functionSignature} returned no txid: ${JSON.stringify(receipt)}`);
|
|
131
|
+
}
|
|
132
|
+
return txid;
|
|
133
|
+
}
|
|
134
|
+
function isRecord(value) {
|
|
135
|
+
return typeof value === 'object' && value !== null;
|
|
136
|
+
}
|
|
137
|
+
function hasSignatureArray(value) {
|
|
138
|
+
return isRecord(value) && Array.isArray(value.signature);
|
|
139
|
+
}
|
|
140
|
+
function normalizeSignedTransaction(value, functionSignature) {
|
|
141
|
+
if (hasSignatureArray(value))
|
|
142
|
+
return value;
|
|
143
|
+
if (isRecord(value)) {
|
|
144
|
+
for (const key of ['transaction', 'signedTransaction', 'signed_transaction', 'tx']) {
|
|
145
|
+
const nested = value[key];
|
|
146
|
+
if (hasSignatureArray(nested))
|
|
147
|
+
return nested;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Wallet returned an unsigned or unsupported transaction for ${functionSignature}; ` +
|
|
151
|
+
'expected a signed Tron transaction object with a signature array.');
|
|
152
|
+
}
|
|
153
|
+
function withTimeout(promise, timeoutMs, message) {
|
|
154
|
+
let timer;
|
|
155
|
+
const timeout = new Promise((_, reject) => {
|
|
156
|
+
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
157
|
+
});
|
|
158
|
+
return Promise.race([promise, timeout]).finally(() => {
|
|
159
|
+
if (timer)
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Shared build → sign → broadcast over any {InjectedTronWeb} (the dApp-broadcast
|
|
165
|
+
* model). TronLink prompts on `trx.sign`; a key-bearing TronWeb signs silently.
|
|
166
|
+
*/
|
|
167
|
+
export function sendCallVia(tw, issuerBase58, call, opts = {}) {
|
|
168
|
+
return sendCallSigned(tw, issuerBase58, call, (t) => tw.trx.sign(t), opts);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* A {SourceSigner} backed by an {AdapterWallet} (WalletConnect / adapter) for signing,
|
|
172
|
+
* plus a lazily-provided node TronWeb for build + broadcast. The node builds the tx
|
|
173
|
+
* against the bridge's chain, so the tx is bound to that chain regardless of what the
|
|
174
|
+
* remote wallet reports — hence `getNetwork()` returns the configured chainId (as
|
|
175
|
+
* {ManagedTronSigner} does), and the wrong-network guard is satisfied by construction.
|
|
176
|
+
*/
|
|
177
|
+
export class AdapterTronSigner {
|
|
178
|
+
wallet;
|
|
179
|
+
tronWebProvider;
|
|
180
|
+
chainId;
|
|
181
|
+
address = null;
|
|
182
|
+
tw = null;
|
|
183
|
+
constructor(wallet,
|
|
184
|
+
/** Lazily builds the node TronWeb (keeps the heavy `tronweb` import off the hot path). */
|
|
185
|
+
tronWebProvider, chainId) {
|
|
186
|
+
this.wallet = wallet;
|
|
187
|
+
this.tronWebProvider = tronWebProvider;
|
|
188
|
+
this.chainId = chainId;
|
|
189
|
+
}
|
|
190
|
+
async connect() {
|
|
191
|
+
this.address = await this.wallet.connect();
|
|
192
|
+
return this.address;
|
|
193
|
+
}
|
|
194
|
+
async getAddress() {
|
|
195
|
+
return this.address ?? this.connect();
|
|
196
|
+
}
|
|
197
|
+
async getNetwork() {
|
|
198
|
+
return this.chainId;
|
|
199
|
+
}
|
|
200
|
+
async sendCall(call, opts) {
|
|
201
|
+
const tw = (this.tw ??= await this.tronWebProvider());
|
|
202
|
+
const issuer = await this.getAddress();
|
|
203
|
+
return sendCallSigned(tw, issuer, call, (t) => this.wallet.signTransaction(t), opts);
|
|
204
|
+
}
|
|
205
|
+
onChange(cb) {
|
|
206
|
+
return this.wallet.onChange?.(cb) ?? (() => { });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Managed-key / WalletConnect signer (06 §W4): wraps a pre-built `TronWeb`-shaped
|
|
211
|
+
* object that already holds a key (managed: `new TronWeb({ privateKey })`; the
|
|
212
|
+
* proven `demo/tron.ts` path) or a WalletConnect-provided signer. No extension,
|
|
213
|
+
* no flow change — same `SourceSigner` surface, signs silently. Its network is
|
|
214
|
+
* known from construction (the node it was built against), so it's trusted.
|
|
215
|
+
*/
|
|
216
|
+
export class ManagedTronSigner {
|
|
217
|
+
tw;
|
|
218
|
+
chainId;
|
|
219
|
+
constructor(tw, chainId) {
|
|
220
|
+
this.tw = tw;
|
|
221
|
+
this.chainId = chainId;
|
|
222
|
+
}
|
|
223
|
+
async connect() {
|
|
224
|
+
return this.getAddress();
|
|
225
|
+
}
|
|
226
|
+
async getAddress() {
|
|
227
|
+
const addr = this.tw.defaultAddress.base58;
|
|
228
|
+
if (!addr)
|
|
229
|
+
throw new Error('ManagedTronSigner: TronWeb has no default address (no key configured).');
|
|
230
|
+
return addr;
|
|
231
|
+
}
|
|
232
|
+
async getNetwork() {
|
|
233
|
+
return this.chainId;
|
|
234
|
+
}
|
|
235
|
+
sendCall(call, opts) {
|
|
236
|
+
return this.getAddress().then((a) => sendCallVia(this.tw, a, call, opts));
|
|
237
|
+
}
|
|
238
|
+
}
|
package/lib/value.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const WALLET_VALUE_TAG = 39050n;
|
|
2
|
+
export declare const WALLET_VALUE_VERSION = 1n;
|
|
3
|
+
/**
|
|
4
|
+
* Reads the bridged-coin amount declared in a token's `data`, or null if the
|
|
5
|
+
* token declares no value for `coinId`. Injected into the verifier so the
|
|
6
|
+
* mint-reason check can confirm the token's declared value equals the locked
|
|
7
|
+
* amount.
|
|
8
|
+
*/
|
|
9
|
+
export type BridgedAmountExtractor = (data: Uint8Array | null, coinId: Uint8Array) => bigint | null;
|
|
10
|
+
/** Encode a single-asset value payload in the wallet format, no memo. */
|
|
11
|
+
export declare function encodeBridgePaymentData(coinId: Uint8Array, amount: bigint): Uint8Array;
|
|
12
|
+
/**
|
|
13
|
+
* Decode the wallet-format payload and return the amount it declares for
|
|
14
|
+
* `coinId`; null when the bytes are not that format, the version is unknown, or
|
|
15
|
+
* the coin is absent.
|
|
16
|
+
*/
|
|
17
|
+
export declare function decodeBridgePaymentData(data: Uint8Array | null, coinId: Uint8Array): bigint | null;
|
|
18
|
+
/**
|
|
19
|
+
* Minimal self-contained value envelope used by the CLI/tests only:
|
|
20
|
+
* `CBOR [ coinId: bstr, amount: uint ]`. Never appears on a real bridged token.
|
|
21
|
+
*/
|
|
22
|
+
export declare function encodeBridgedValue(coinId: Uint8Array, amount: bigint): Uint8Array;
|
|
23
|
+
export declare function decodeBridgedValue(data: Uint8Array | null, coinId: Uint8Array): bigint | null;
|
package/lib/value.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
import { Asset } from '@unicitylabs/state-transition-sdk/lib/payment/asset/Asset.js';
|
|
4
|
+
import { AssetId } from '@unicitylabs/state-transition-sdk/lib/payment/asset/AssetId.js';
|
|
5
|
+
import { PaymentAssetCollection } from '@unicitylabs/state-transition-sdk/lib/payment/asset/PaymentAssetCollection.js';
|
|
6
|
+
import { bytesEqual } from './hex.js';
|
|
7
|
+
export const WALLET_VALUE_TAG = 39050n;
|
|
8
|
+
export const WALLET_VALUE_VERSION = 1n;
|
|
9
|
+
/** Encode a single-asset value payload in the wallet format, no memo. */
|
|
10
|
+
export function encodeBridgePaymentData(coinId, amount) {
|
|
11
|
+
const assets = PaymentAssetCollection.create(new Asset(new AssetId(coinId), amount));
|
|
12
|
+
return CborSerializer.encodeTag(WALLET_VALUE_TAG, CborSerializer.encodeArray(CborSerializer.encodeUnsignedInteger(WALLET_VALUE_VERSION), assets.toCBOR(), CborSerializer.encodeNullable(null, CborSerializer.encodeByteString)));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Decode the wallet-format payload and return the amount it declares for
|
|
16
|
+
* `coinId`; null when the bytes are not that format, the version is unknown, or
|
|
17
|
+
* the coin is absent.
|
|
18
|
+
*/
|
|
19
|
+
export function decodeBridgePaymentData(data, coinId) {
|
|
20
|
+
if (!data)
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const tag = CborDeserializer.decodeTag(data);
|
|
24
|
+
if (tag.tag !== WALLET_VALUE_TAG)
|
|
25
|
+
return null;
|
|
26
|
+
const fields = CborDeserializer.decodeArray(tag.data, 3);
|
|
27
|
+
if (CborDeserializer.decodeUnsignedInteger(fields[0]) !== WALLET_VALUE_VERSION)
|
|
28
|
+
return null;
|
|
29
|
+
return PaymentAssetCollection.fromCBOR(fields[1]).get(new AssetId(coinId))?.value ?? null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Minimal self-contained value envelope used by the CLI/tests only:
|
|
37
|
+
* `CBOR [ coinId: bstr, amount: uint ]`. Never appears on a real bridged token.
|
|
38
|
+
*/
|
|
39
|
+
export function encodeBridgedValue(coinId, amount) {
|
|
40
|
+
return CborSerializer.encodeArray(CborSerializer.encodeByteString(coinId), CborSerializer.encodeUnsignedInteger(amount));
|
|
41
|
+
}
|
|
42
|
+
export function decodeBridgedValue(data, coinId) {
|
|
43
|
+
if (!data) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
let items;
|
|
47
|
+
try {
|
|
48
|
+
items = CborDeserializer.decodeArray(data, 2);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const encodedCoinId = CborDeserializer.decodeByteString(items[0]);
|
|
54
|
+
if (!bytesEqual(encodedCoinId, coinId)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return CborDeserializer.decodeUnsignedInteger(items[1]);
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowance query (08 §1.1) — the read that lets bridge-in skip a redundant
|
|
3
|
+
* `approve`. Pure ABI encoding over a {ConstantCaller}; no chain library, no
|
|
4
|
+
* wallet involvement (it's a node read, not a signed tx). Kept in the plugin so
|
|
5
|
+
* Sphere never encodes a contract call.
|
|
6
|
+
*/
|
|
7
|
+
import type { ConstantCaller } from '../source-chain.js';
|
|
8
|
+
export interface AllowanceQuery {
|
|
9
|
+
/** Token contract, in any address form of its chain. */
|
|
10
|
+
readonly assetAddress: string;
|
|
11
|
+
/** Token holder (the wallet). */
|
|
12
|
+
readonly owner: string;
|
|
13
|
+
/** Spender (the vault). */
|
|
14
|
+
readonly spender: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read `allowance(owner, spender)` off the token contract. Returns the current
|
|
18
|
+
* approved amount in the asset's smallest unit; `0n` when nothing is approved.
|
|
19
|
+
*/
|
|
20
|
+
export declare function queryAllowance(rpc: ConstantCaller, q: AllowanceQuery): Promise<bigint>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { toEvmAddressHex } from '../address.js';
|
|
2
|
+
/** Left-pad a 20-byte EVM-form hex address to a 32-byte ABI word. */
|
|
3
|
+
function addressWord(evmHex) {
|
|
4
|
+
const h = evmHex.replace(/^0x/i, '').toLowerCase();
|
|
5
|
+
return h.padStart(64, '0');
|
|
6
|
+
}
|
|
7
|
+
/** Parse a 32-byte ABI uint256 word (hex, no `0x`) to a bigint. */
|
|
8
|
+
function wordToBigInt(word) {
|
|
9
|
+
const h = word.replace(/^0x/i, '');
|
|
10
|
+
return h ? BigInt('0x' + h) : 0n;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read `allowance(owner, spender)` off the token contract. Returns the current
|
|
14
|
+
* approved amount in the asset's smallest unit; `0n` when nothing is approved.
|
|
15
|
+
*/
|
|
16
|
+
export async function queryAllowance(rpc, q) {
|
|
17
|
+
const ownerHex = toEvmAddressHex(q.owner);
|
|
18
|
+
const spenderHex = toEvmAddressHex(q.spender);
|
|
19
|
+
const assetHex = toEvmAddressHex(q.assetAddress);
|
|
20
|
+
const word = await rpc.constantCall({
|
|
21
|
+
ownerHex,
|
|
22
|
+
contractHex: assetHex,
|
|
23
|
+
functionSignature: 'allowance(address,address)',
|
|
24
|
+
parameterHex: addressWord(ownerHex) + addressWord(spenderHex),
|
|
25
|
+
});
|
|
26
|
+
return wordToBigInt(word);
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { toHex } from '../hex.js';
|
|
2
|
+
import { BridgeLockJustification } from '../BridgeLockJustification.js';
|
|
3
|
+
export function mintedAgainst(bridge, justification) {
|
|
4
|
+
if (!justification)
|
|
5
|
+
return false;
|
|
6
|
+
let lock;
|
|
7
|
+
try {
|
|
8
|
+
lock = BridgeLockJustification.fromCBOR(justification);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const cfg = bridge.plugin.resolvedConfig;
|
|
14
|
+
return lock.data.chainId === cfg.chainId && toHex(lock.data.lockContract).toLowerCase() === cfg.lockContractHex;
|
|
15
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Token } from '@unicitylabs/state-transition-sdk/lib/transaction/Token.js';
|
|
2
|
+
import { type BridgeBackReason, type BridgeConfig } from '../bridge-back/derivations.js';
|
|
3
|
+
import { type BridgeBackBurnReason, createBridgeBackBurnTransfer, type ReturnPreview, type WitnessRequest } from '../bridge-back/burn.js';
|
|
4
|
+
import { fromHex, toHex } from '../hex.js';
|
|
5
|
+
import type { ContractCall } from '../contract-call.js';
|
|
6
|
+
import type { BridgePlugin } from '../index.js';
|
|
7
|
+
export * from './manifest.js';
|
|
8
|
+
export { buildBridgeBackBurnReason } from '../bridge-back/burn.js';
|
|
9
|
+
export type { BridgeBackBurnReason } from '../bridge-back/burn.js';
|
|
10
|
+
export { previewReturn } from '../bridge-back/burn.js';
|
|
11
|
+
export { decodeBridgeBackReason } from '../bridge-back/derivations.js';
|
|
12
|
+
export type { BridgeBackReason, BridgeConfig, DecodedBridgeBackReason, ReturnLeaf, } from '../bridge-back/derivations.js';
|
|
13
|
+
export type { CallParam, ContractCall } from '../contract-call.js';
|
|
14
|
+
/** Everything bridge-in needs after the user picks asset + amount (06 §A1.1). */
|
|
15
|
+
export interface BridgeInPlan {
|
|
16
|
+
/** The Unicity TokenId this deposit funds (hex). The lock commits to it. */
|
|
17
|
+
readonly tokenIdHex: string;
|
|
18
|
+
/** Salt the wallet keeps to mint exactly this token (hex, 32 bytes). */
|
|
19
|
+
readonly saltHex: string;
|
|
20
|
+
/** `SHA256(ownerPredicateCbor)` committed on Tron (hex, 32 bytes). */
|
|
21
|
+
readonly recipientCommitmentHex: string;
|
|
22
|
+
/** Locked amount in the asset's smallest unit. */
|
|
23
|
+
readonly amount: bigint;
|
|
24
|
+
/** One-time max `approve` of the asset to the vault (skip if allowance already covers `amount`). */
|
|
25
|
+
readonly approve: ContractCall;
|
|
26
|
+
/** `lock(amount, tokenId, recipientCommitment)` on the vault. */
|
|
27
|
+
readonly lock: ContractCall;
|
|
28
|
+
}
|
|
29
|
+
/** Inputs to {buildBridgeInPlan}. Provide exactly one of `recipientPubkey` / `ownerPredicateCbor`. */
|
|
30
|
+
export interface BridgeInPlanInput {
|
|
31
|
+
readonly plugin: BridgePlugin;
|
|
32
|
+
readonly amount: bigint;
|
|
33
|
+
/** Network id of the Unicity network the token is minted on (e.g. testnet2 = 4). */
|
|
34
|
+
readonly networkId: number;
|
|
35
|
+
/**
|
|
36
|
+
* The wallet's 33-byte compressed chain pubkey. The façade builds the same
|
|
37
|
+
* `SignaturePredicate(pubkey)` the engine mints to, so the lock's
|
|
38
|
+
* `recipientCommitment` binds the bridged token to this wallet. Preferred — the
|
|
39
|
+
* app never needs the SDK.
|
|
40
|
+
*/
|
|
41
|
+
readonly recipientPubkey?: Uint8Array;
|
|
42
|
+
/** Alternative: a pre-encoded owner `EncodedPredicate` CBOR (commitment = SHA256 of it). */
|
|
43
|
+
readonly ownerPredicateCbor?: Uint8Array;
|
|
44
|
+
/**
|
|
45
|
+
* `approve` amount. A one-time `MAX_UINT256` reduces repeat bridges to a single
|
|
46
|
+
* prompt (06 §A1.3); default exact `amount`.
|
|
47
|
+
*/
|
|
48
|
+
readonly approveAmount?: bigint;
|
|
49
|
+
}
|
|
50
|
+
declare const MAX_UINT256: bigint;
|
|
51
|
+
/**
|
|
52
|
+
* Derive the bridge-in target token + the unsigned `approve`/`lock` calls, in
|
|
53
|
+
* the family-neutral {ContractCall} shape the wallet's signer encodes for its
|
|
54
|
+
* chain. Pure + offline (no RPC): the order is load-bearing — the lock commits
|
|
55
|
+
* to this exact `tokenId` + `recipientCommitment`, and the wallet then mints
|
|
56
|
+
* *that* token.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildBridgeInPlan(input: BridgeInPlanInput): Promise<BridgeInPlan>;
|
|
59
|
+
/** Convenience: the max-allowance approve amount (one-time approve UX). */
|
|
60
|
+
export { MAX_UINT256 };
|
|
61
|
+
/**
|
|
62
|
+
* Build the terminal burn for a bridge-back (06 §A1.2 step 3). The caller
|
|
63
|
+
* certifies the returned transfer through its normal SDK path, then calls
|
|
64
|
+
* {finalizeBridgeBack} with the certified burn's stateId/txHash. For a partial
|
|
65
|
+
* return, split first and pass the child whose value == `reason.amount`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildBridgeBackBurn(args: {
|
|
68
|
+
readonly token: Token;
|
|
69
|
+
readonly bridgeConfig: BridgeConfig;
|
|
70
|
+
readonly reason: BridgeBackReason;
|
|
71
|
+
readonly stateMask: Uint8Array;
|
|
72
|
+
}): Promise<{
|
|
73
|
+
transfer: Awaited<ReturnType<typeof createBridgeBackBurnTransfer>>['transfer'];
|
|
74
|
+
reason: BridgeBackBurnReason;
|
|
75
|
+
}>;
|
|
76
|
+
/** The read-only return preview + the prover hand-off envelope, post-certification. */
|
|
77
|
+
export interface BridgeBackArtifacts {
|
|
78
|
+
readonly preview: ReturnPreview;
|
|
79
|
+
readonly witnessRequest: WitnessRequest;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* After the burn certifies, derive the pending nullifier + settlement leaf
|
|
83
|
+
* ({previewReturn}) and assemble the prover hand-off envelope
|
|
84
|
+
* ({buildWitnessRequest}). The wallet shows `preview` and POSTs `witnessRequest`
|
|
85
|
+
* (plus the burned-token blob, recovery-critical) to the return service.
|
|
86
|
+
* `reasonBytes` are the canonical bytes from {buildBridgeBackBurn}'s reason.
|
|
87
|
+
*/
|
|
88
|
+
export declare function finalizeBridgeBack(args: {
|
|
89
|
+
readonly configHash: Uint8Array;
|
|
90
|
+
readonly reason: BridgeBackReason;
|
|
91
|
+
readonly reasonBytes: Uint8Array;
|
|
92
|
+
readonly burnStateId: Uint8Array;
|
|
93
|
+
readonly burnTxHash: Uint8Array;
|
|
94
|
+
readonly burnedTokenCbor: Uint8Array;
|
|
95
|
+
}): BridgeBackArtifacts;
|
|
96
|
+
/** Hex helpers re-exported for read-only UI formatting. */
|
|
97
|
+
export { fromHex, toHex };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet-facing façade (06 §W0) — the only surface Sphere calls.
|
|
3
|
+
*
|
|
4
|
+
* Boundary (decision #2): Sphere never recomputes a bridge hash or touches a
|
|
5
|
+
* chain-specific detail. It loads a {BridgeManifest}, asks this façade to derive
|
|
6
|
+
* the bridge-in plan / build the bridge-back artifacts, and renders UI. Every
|
|
7
|
+
* value here is either a pure derivation (conformance-tested against
|
|
8
|
+
* `protocol/vectors`) or a thin wrapper over an already-built bridge-back function.
|
|
9
|
+
*/
|
|
10
|
+
import { NetworkId } from '@unicitylabs/state-transition-sdk/lib/api/NetworkId.js';
|
|
11
|
+
import { EncodedPredicate } from '@unicitylabs/state-transition-sdk/lib/predicate/EncodedPredicate.js';
|
|
12
|
+
import { SignaturePredicate } from '@unicitylabs/state-transition-sdk/lib/predicate/builtin/SignaturePredicate.js';
|
|
13
|
+
import { TokenId } from '@unicitylabs/state-transition-sdk/lib/transaction/TokenId.js';
|
|
14
|
+
import { TokenSalt } from '@unicitylabs/state-transition-sdk/lib/transaction/TokenSalt.js';
|
|
15
|
+
import { buildWitnessRequest, createBridgeBackBurnTransfer, previewReturn, } from '../bridge-back/burn.js';
|
|
16
|
+
import { recipientCommitment } from '../identifiers.js';
|
|
17
|
+
import { fromHex, toHex } from '../hex.js';
|
|
18
|
+
export * from './manifest.js';
|
|
19
|
+
// Pure bridge-back reason builder (reasonBytes + reasonHash) — the app feeds these
|
|
20
|
+
// to the engine's bridgeBurn, no SDK token needed (06 §A1.2).
|
|
21
|
+
export { buildBridgeBackBurnReason } from '../bridge-back/burn.js';
|
|
22
|
+
// Read-only bridge-back surface the UI renders directly.
|
|
23
|
+
export { previewReturn } from '../bridge-back/burn.js';
|
|
24
|
+
export { decodeBridgeBackReason } from '../bridge-back/derivations.js';
|
|
25
|
+
const MAX_UINT256 = (1n << 256n) - 1n;
|
|
26
|
+
/**
|
|
27
|
+
* Derive the bridge-in target token + the unsigned `approve`/`lock` calls, in
|
|
28
|
+
* the family-neutral {ContractCall} shape the wallet's signer encodes for its
|
|
29
|
+
* chain. Pure + offline (no RPC): the order is load-bearing — the lock commits
|
|
30
|
+
* to this exact `tokenId` + `recipientCommitment`, and the wallet then mints
|
|
31
|
+
* *that* token.
|
|
32
|
+
*/
|
|
33
|
+
export async function buildBridgeInPlan(input) {
|
|
34
|
+
const saltBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
35
|
+
const salt = TokenSalt.fromBytes(saltBytes);
|
|
36
|
+
const tokenId = await TokenId.fromSalt(NetworkId.fromId(input.networkId), salt);
|
|
37
|
+
const ownerCbor = input.ownerPredicateCbor ??
|
|
38
|
+
(input.recipientPubkey
|
|
39
|
+
? EncodedPredicate.fromPredicate(SignaturePredicate.create(input.recipientPubkey)).toCBOR()
|
|
40
|
+
: undefined);
|
|
41
|
+
if (!ownerCbor) {
|
|
42
|
+
throw new Error('buildBridgeInPlan: provide recipientPubkey or ownerPredicateCbor');
|
|
43
|
+
}
|
|
44
|
+
const commitment = recipientCommitment(ownerCbor);
|
|
45
|
+
const vaultHex = input.plugin.resolvedConfig.lockContractHex;
|
|
46
|
+
const assetHex = input.plugin.resolvedConfig.assetContractHex;
|
|
47
|
+
const approveAmount = input.approveAmount ?? input.amount;
|
|
48
|
+
return {
|
|
49
|
+
tokenIdHex: toHex(tokenId.bytes),
|
|
50
|
+
saltHex: toHex(saltBytes),
|
|
51
|
+
recipientCommitmentHex: toHex(commitment),
|
|
52
|
+
amount: input.amount,
|
|
53
|
+
approve: {
|
|
54
|
+
contractHex: assetHex,
|
|
55
|
+
functionSignature: 'approve(address,uint256)',
|
|
56
|
+
parameters: [
|
|
57
|
+
{ type: 'address', value: vaultHex },
|
|
58
|
+
{ type: 'uint256', value: approveAmount.toString() },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
lock: {
|
|
62
|
+
contractHex: vaultHex,
|
|
63
|
+
functionSignature: 'lock(uint256,bytes32,bytes32)',
|
|
64
|
+
parameters: [
|
|
65
|
+
{ type: 'uint256', value: input.amount.toString() },
|
|
66
|
+
{ type: 'bytes32', value: '0x' + toHex(tokenId.bytes) },
|
|
67
|
+
{ type: 'bytes32', value: '0x' + toHex(commitment) },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Convenience: the max-allowance approve amount (one-time approve UX). */
|
|
73
|
+
export { MAX_UINT256 };
|
|
74
|
+
/**
|
|
75
|
+
* Build the terminal burn for a bridge-back (06 §A1.2 step 3). The caller
|
|
76
|
+
* certifies the returned transfer through its normal SDK path, then calls
|
|
77
|
+
* {finalizeBridgeBack} with the certified burn's stateId/txHash. For a partial
|
|
78
|
+
* return, split first and pass the child whose value == `reason.amount`.
|
|
79
|
+
*/
|
|
80
|
+
export async function buildBridgeBackBurn(args) {
|
|
81
|
+
return createBridgeBackBurnTransfer(args.token, args.bridgeConfig, args.reason, args.stateMask);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* After the burn certifies, derive the pending nullifier + settlement leaf
|
|
85
|
+
* ({previewReturn}) and assemble the prover hand-off envelope
|
|
86
|
+
* ({buildWitnessRequest}). The wallet shows `preview` and POSTs `witnessRequest`
|
|
87
|
+
* (plus the burned-token blob, recovery-critical) to the return service.
|
|
88
|
+
* `reasonBytes` are the canonical bytes from {buildBridgeBackBurn}'s reason.
|
|
89
|
+
*/
|
|
90
|
+
export function finalizeBridgeBack(args) {
|
|
91
|
+
const preview = previewReturn(args.configHash, args.reason, args.burnStateId, args.burnTxHash);
|
|
92
|
+
const witnessRequest = buildWitnessRequest({
|
|
93
|
+
tokenCbor: args.burnedTokenCbor,
|
|
94
|
+
configHash: args.configHash,
|
|
95
|
+
reasonBytes: args.reasonBytes,
|
|
96
|
+
});
|
|
97
|
+
return { preview, witnessRequest };
|
|
98
|
+
}
|
|
99
|
+
/** Hex helpers re-exported for read-only UI formatting. */
|
|
100
|
+
export { fromHex, toHex };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LoadedBridge } from './manifest.js';
|
|
2
|
+
export interface LockFinality {
|
|
3
|
+
readonly confirmations: bigint;
|
|
4
|
+
readonly required: number;
|
|
5
|
+
readonly final: boolean;
|
|
6
|
+
readonly secondsLeft: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function lockFinality(bridge: LoadedBridge, justification: Uint8Array | null): Promise<LockFinality | null>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BridgeLockJustification } from '../BridgeLockJustification.js';
|
|
2
|
+
import { chainFamily } from '../families.js';
|
|
3
|
+
import { toHex } from '../hex.js';
|
|
4
|
+
export async function lockFinality(bridge, justification) {
|
|
5
|
+
if (!justification)
|
|
6
|
+
return null;
|
|
7
|
+
let lock;
|
|
8
|
+
try {
|
|
9
|
+
lock = BridgeLockJustification.fromCBOR(justification);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const cfg = bridge.plugin.resolvedConfig;
|
|
15
|
+
if (lock.data.chainId !== cfg.chainId || toHex(lock.data.lockContract).toLowerCase() !== cfg.lockContractHex)
|
|
16
|
+
return null;
|
|
17
|
+
const required = cfg.confirmations;
|
|
18
|
+
const blockSeconds = chainFamily(cfg.family).blockSeconds;
|
|
19
|
+
const info = await bridge.plugin.rpc.getTransactionInfo(toHex(lock.data.txid));
|
|
20
|
+
const confirmations = info ? (await bridge.plugin.rpc.getNowBlockNumber()) - info.blockNumber : 0n;
|
|
21
|
+
const missing = BigInt(required) - confirmations;
|
|
22
|
+
return {
|
|
23
|
+
confirmations,
|
|
24
|
+
required,
|
|
25
|
+
final: missing <= 0n,
|
|
26
|
+
secondsLeft: missing <= 0n ? 0 : Number(missing) * blockSeconds,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet integration surface (06 §A2.2). The single entry point a wallet (Sphere)
|
|
3
|
+
* imports — façade (plan builders), manifest (loader/type), the family-neutral
|
|
4
|
+
* signer contract and each family's wallets. Kept as a subpath, *not* merged
|
|
5
|
+
* into the package root, so the read-only re-exports here don't collide with
|
|
6
|
+
* the root's bridge-back exports.
|
|
7
|
+
*/
|
|
8
|
+
export * from './facade.js';
|
|
9
|
+
export * from './manifests.js';
|
|
10
|
+
export * from './registry.js';
|
|
11
|
+
export * from './signer.js';
|
|
12
|
+
export * from '../tron/signer.js';
|
|
13
|
+
export * from '../tron/providers.js';
|
|
14
|
+
export * from '../tron/presentation.js';
|
|
15
|
+
export * from '../evm/signer.js';
|
|
16
|
+
export * from '../evm/providers.js';
|
|
17
|
+
export * from '../evm/presentation.js';
|
|
18
|
+
export * from './allowance.js';
|
|
19
|
+
export * from './source-adapter.js';
|
|
20
|
+
export * from './return-client.js';
|
|
21
|
+
export * from './self-mint-verifier.js';
|
|
22
|
+
export * from './token-plugin.js';
|
|
23
|
+
export * from './backing.js';
|
|
24
|
+
export * from './finality.js';
|
|
25
|
+
export * from './payout.js';
|