Aegis: Tron signing overlay comes from the bytes being signed
The overlay for tronWeb-built transactions was built from the dapp's raw_data JSON, which need not match raw_data_hex: a site could show "1 TRX to X" and get a signature over anything. lib/tron-decode.js decodes Transaction.raw from raw_data_hex (contract type, owner, to, amount, TRC-20 transfer/approve calldata, fee limit, memo); the txID must match the bytes, every contract's owner must be this wallet, and unlimited approvals or permission/resource delegation get a danger action. sendRawTransaction relayed any signed transaction a site handed it; it now broadcasts only txids Aegis itself signed.
This commit is contained in:
parent
0e7d6cc3c4
commit
206f54edd2
2 changed files with 226 additions and 23 deletions
|
|
@ -59,6 +59,9 @@ async function loadDeps(api) {
|
||||||
const siaAdapter = require("./lib/chain-sia.js")({ ed25519, blake2b });
|
const siaAdapter = require("./lib/chain-sia.js")({ ed25519, blake2b });
|
||||||
const ethAdapter = require("./lib/chain-eth.js")({ HDKey, secp256k1, keccak_256 });
|
const ethAdapter = require("./lib/chain-eth.js")({ HDKey, secp256k1, keccak_256 });
|
||||||
const eip712 = require("./lib/eip712.js")({ keccak_256 });
|
const eip712 = require("./lib/eip712.js")({ keccak_256 });
|
||||||
|
// Tron raw_data_hex decoder — the approval overlay for dapp-built Tron
|
||||||
|
// transactions is built from these bytes, never from the dapp's JSON.
|
||||||
|
const tronDecode = require("./lib/tron-decode.js")({ sha256, base58check });
|
||||||
// Solana uses the raw base58 alphabet (no checksum), which lives inside
|
// Solana uses the raw base58 alphabet (no checksum), which lives inside
|
||||||
// base58check as encodeBase58 / decodeBase58 — expose them under a
|
// base58check as encodeBase58 / decodeBase58 — expose them under a
|
||||||
// `{encode, decode}` shape the SOL adapter reads from.
|
// `{encode, decode}` shape the SOL adapter reads from.
|
||||||
|
|
@ -144,7 +147,7 @@ async function loadDeps(api) {
|
||||||
bchAdapter, tronAdapter, siaAdapter, dgbAdapter, ethAdapter, solAdapter, btcAdapter,
|
bchAdapter, tronAdapter, siaAdapter, dgbAdapter, ethAdapter, solAdapter, btcAdapter,
|
||||||
importedBchAdapter, utxoImportedAdapter, genericImportedAdapter,
|
importedBchAdapter, utxoImportedAdapter, genericImportedAdapter,
|
||||||
derive, bip39,
|
derive, bip39,
|
||||||
dgbCore, dgbPsbt, bitcoinjs, ecc, eip712,
|
dgbCore, dgbPsbt, bitcoinjs, ecc, eip712, tronDecode,
|
||||||
wcCore, wcWallet, libauth };
|
wcCore, wcWallet, libauth };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2670,6 +2673,14 @@ function grantsForPanel(api) {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tron: only transactions Aegis itself signed may be broadcast through the
|
||||||
|
// bridge — a dapp can't use the wallet as a relay for foreign signatures.
|
||||||
|
const signedTronTxids = new Set();
|
||||||
|
function rememberSignedTron(txid) {
|
||||||
|
signedTronTxids.add(txid);
|
||||||
|
if (signedTronTxids.size > 200) signedTronTxids.delete(signedTronTxids.values().next().value);
|
||||||
|
}
|
||||||
|
|
||||||
// Dapp message bytes: personal_sign carries hex-encoded bytes (ethers, viem,
|
// Dapp message bytes: personal_sign carries hex-encoded bytes (ethers, viem,
|
||||||
// wagmi); a plain string is UTF-8 (older dapps, our own panel).
|
// wagmi); a plain string is UTF-8 (older dapps, our own panel).
|
||||||
function bytesFromDappMessage(raw) {
|
function bytesFromDappMessage(raw) {
|
||||||
|
|
@ -3037,38 +3048,64 @@ function registerPageMessages(api) {
|
||||||
return { address: snap.address, network: snap.network };
|
return { address: snap.address, network: snap.network };
|
||||||
});
|
});
|
||||||
// Sign an arbitrary raw_data_hex the dapp built (with its own tronWeb).
|
// Sign an arbitrary raw_data_hex the dapp built (with its own tronWeb).
|
||||||
// The wallet never guesses the intent — the approval overlay shows the
|
// Everything the overlay shows is decoded from raw_data_hex — the bytes
|
||||||
// decoded contract type and destination when it can, and always the txID.
|
// that get signed. The overlay used to read the dapp's raw_data JSON,
|
||||||
|
// which can say "1 TRX to X" over bytes that do something else entirely.
|
||||||
api.onMessage("trx.signTransaction", async (p, m) => {
|
api.onMessage("trx.signTransaction", async (p, m) => {
|
||||||
const origin = fromPage(m);
|
const origin = fromPage(m);
|
||||||
const rt = activeTronRuntime();
|
const rt = activeTronRuntime();
|
||||||
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
|
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
|
||||||
const tx = p && p.transaction;
|
const tx = p && p.transaction;
|
||||||
if (!tx || typeof tx !== "object" || !tx.raw_data_hex || !tx.raw_data) throw new Error("bad transaction");
|
if (!tx || typeof tx !== "object" || !tx.raw_data_hex) throw new Error("bad transaction");
|
||||||
|
// The dapp's raw_data JSON and txID are cross-checked, never trusted.
|
||||||
|
let decoded;
|
||||||
|
try { decoded = ctx.d.tronDecode.decodeRawData(tx.raw_data_hex); }
|
||||||
|
catch (e) { throw new Error("cannot decode raw_data_hex: " + (e?.message || e)); }
|
||||||
|
if (tx.txID && String(tx.txID).toLowerCase() !== decoded.txid) throw new Error("txID does not match raw_data_hex");
|
||||||
|
const me = rt.adapter.address;
|
||||||
|
for (const c of decoded.contracts) {
|
||||||
|
if (!c.owner) throw new Error(`cannot read the owner of ${c.typeName}; refusing to sign`);
|
||||||
|
if (c.owner !== me) throw new Error(`transaction owner ${c.owner} is not this wallet`);
|
||||||
|
}
|
||||||
return withOriginLock(origin, async () => {
|
return withOriginLock(origin, async () => {
|
||||||
const contract = (tx.raw_data.contract || [])[0];
|
const rows = [];
|
||||||
const type = contract?.type || "Contract";
|
decoded.contracts.forEach((c, i) => {
|
||||||
const rows = [{ label: "Type", value: type }, { label: "Tx ID", value: tx.txID || "(unset)", mono: true }];
|
const n = decoded.contracts.length > 1 ? ` #${i + 1}` : "";
|
||||||
if (type === "TransferContract") {
|
rows.push({ label: "Type" + n, value: c.typeName + (c.dangerous ? " — grants control to another account" : "") });
|
||||||
const v = contract.parameter?.value || {};
|
if (c.type === 1) {
|
||||||
try {
|
rows.push({ label: "To", value: c.to || "(none)", mono: true });
|
||||||
const to = v.to_address ? ctx.d.tronAdapter.hexToAddress(v.to_address) : (v.to_address || "");
|
rows.push({ label: "Amount", value: `${fmtTrx(Number(c.amount))} TRX`, strong: true });
|
||||||
const amount = Number(v.amount || 0);
|
} else if (c.type === 2) {
|
||||||
rows.splice(1, 0, { label: "To", value: to, mono: true }, { label: "Amount", value: `${fmtTrx(amount)} TRX`, strong: true });
|
rows.push({ label: "To", value: c.to || "(none)", mono: true });
|
||||||
} catch {}
|
rows.push({ label: "Amount", value: `${String(c.amount)} units of asset ${c.assetName || "?"}`, strong: true });
|
||||||
}
|
} else if (c.type === 31) {
|
||||||
|
rows.push({ label: "Contract", value: c.contract || "(none)", mono: true });
|
||||||
|
const call = c.call;
|
||||||
|
if (call && call.name === "transfer") rows.push({ label: "Call", value: `transfer ${call.unlimited ? "UNLIMITED" : String(call.amount)} token units to ${call.to}`, strong: true });
|
||||||
|
else if (call && (call.name === "approve" || call.name === "increaseAllowance")) rows.push({ label: "Call", value: `${call.name}: let ${call.spender} spend ${call.unlimited ? "UNLIMITED (∞)" : String(call.amount)} token units`, strong: true });
|
||||||
|
else if (call && call.name === "transferFrom") rows.push({ label: "Call", value: `transferFrom ${call.from} → ${call.to}: ${call.unlimited ? "UNLIMITED" : String(call.amount)} units`, strong: true });
|
||||||
|
else rows.push({ label: "Call", value: call && call.selector ? `unknown method 0x${call.selector} (${c.data.length / 2} bytes, not decoded)` : "(no call data)", mono: true });
|
||||||
|
if (c.callValue) rows.push({ label: "TRX attached", value: `${fmtTrx(Number(c.callValue))} TRX` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (decoded.feeLimit != null) rows.push({ label: "Fee limit", value: `${fmtTrx(Number(decoded.feeLimit))} TRX` });
|
||||||
|
if (decoded.memo) rows.push({ label: "Memo", value: decoded.memo.slice(0, 200), mono: true });
|
||||||
|
rows.push({ label: "Tx ID", value: decoded.txid, mono: true });
|
||||||
rows.push({ label: "Wallet", value: `${rt.entry.label} — Tron · ${rt.adapter.snapshot().network === "nile" ? "Nile testnet" : "Mainnet"}` });
|
rows.push({ label: "Wallet", value: `${rt.entry.label} — Tron · ${rt.adapter.snapshot().network === "nile" ? "Nile testnet" : "Mainnet"}` });
|
||||||
|
const risky = decoded.contracts.some((c) => c.dangerous || (c.call && c.call.unlimited));
|
||||||
const pick = await api.approvalModal({
|
const pick = await api.approvalModal({
|
||||||
title: "Sign a Tron transaction?",
|
title: "Sign a Tron transaction?",
|
||||||
origin,
|
origin,
|
||||||
body: "The site built this transaction. Check the type, amount, and destination before signing.",
|
body: risky
|
||||||
|
? "WARNING: this transaction hands another account unlimited or permanent control over funds. Only sign if you fully trust this site."
|
||||||
|
: "Decoded from the bytes that will be signed. Check the type, amount and destination.",
|
||||||
rows,
|
rows,
|
||||||
actions: [{ id: "sign", label: "Sign", primary: true }],
|
actions: [{ id: "sign", label: risky ? "Sign anyway" : "Sign", primary: !risky, danger: risky }],
|
||||||
});
|
});
|
||||||
if (pick !== "sign") throw new Error("user rejected");
|
if (pick !== "sign") throw new Error("user rejected");
|
||||||
const sig = rt.adapter.signRawData(tx.raw_data_hex);
|
const sig = rt.adapter.signRawData(tx.raw_data_hex);
|
||||||
const signed = { ...tx, signature: [sig] };
|
rememberSignedTron(decoded.txid);
|
||||||
return signed;
|
return { ...tx, txID: decoded.txid, signature: [sig] };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
api.onMessage("trx.sendRawTransaction", async (p, m) => {
|
api.onMessage("trx.sendRawTransaction", async (p, m) => {
|
||||||
|
|
@ -3077,10 +3114,13 @@ function registerPageMessages(api) {
|
||||||
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
|
if (!grantFor(api, origin, "trx")) throw new Error("not connected — call tron_requestAccounts first");
|
||||||
const signedTx = p && p.transaction;
|
const signedTx = p && p.transaction;
|
||||||
if (!signedTx || !signedTx.raw_data_hex || !Array.isArray(signedTx.signature)) throw new Error("bad signed tx");
|
if (!signedTx || !signedTx.raw_data_hex || !Array.isArray(signedTx.signature)) throw new Error("bad signed tx");
|
||||||
// No approval here — broadcasting a *signed* tx does not add any risk
|
// No approval here — the sign step carried it. But only what Aegis
|
||||||
// the sign step didn't already carry. Sites that don't want an extra
|
// signed goes out: the bridge is not a relay for foreign signatures.
|
||||||
// network round-trip pass {broadcast:true} to sign; we support both.
|
let txid;
|
||||||
return rt.adapter.broadcastSignedTx(signedTx);
|
try { txid = ctx.d.tronDecode.decodeRawData(signedTx.raw_data_hex).txid; }
|
||||||
|
catch (e) { throw new Error("cannot decode raw_data_hex: " + (e?.message || e)); }
|
||||||
|
if (!signedTronTxids.has(txid)) throw new Error("refusing to broadcast a transaction this wallet did not sign");
|
||||||
|
return rt.adapter.broadcastSignedTx({ ...signedTx, txID: txid });
|
||||||
});
|
});
|
||||||
api.onMessage("trx.signMessageV2", async (p, m) => {
|
api.onMessage("trx.signMessageV2", async (p, m) => {
|
||||||
const origin = fromPage(m);
|
const origin = fromPage(m);
|
||||||
|
|
|
||||||
163
bundled-addons/aegis/lib/tron-decode.js
Normal file
163
bundled-addons/aegis/lib/tron-decode.js
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
// Minimal protobuf wire decoder for Tron's `Transaction.raw` — what a dapp
|
||||||
|
// hands us as `raw_data_hex`. The approval overlay MUST be built from these
|
||||||
|
// bytes (the thing that actually gets signed), never from the dapp's
|
||||||
|
// `raw_data` JSON, which can say anything.
|
||||||
|
//
|
||||||
|
// Field numbers from protocol/core/Tron.proto:
|
||||||
|
// Transaction.raw: 1 ref_block_bytes, 3 ref_block_num, 4 ref_block_hash,
|
||||||
|
// 8 expiration, 9 auths, 10 data, 11 contract (repeated), 12 scripts,
|
||||||
|
// 14 timestamp, 18 fee_limit
|
||||||
|
// Transaction.Contract: 1 type (enum), 2 parameter (google.protobuf.Any),
|
||||||
|
// 3 provider, 4 ContractName, 5 Permission_id
|
||||||
|
// Any: 1 type_url (string), 2 value (bytes)
|
||||||
|
// TransferContract: 1 owner_address, 2 to_address, 3 amount
|
||||||
|
// TransferAssetContract: 1 asset_name, 2 owner_address, 3 to_address, 4 amount
|
||||||
|
// TriggerSmartContract: 1 owner_address, 2 contract_address, 3 call_value,
|
||||||
|
// 4 data, 5 call_token_value, 6 token_id
|
||||||
|
// Every other contract type puts owner_address in field 1.
|
||||||
|
|
||||||
|
const CONTRACT_TYPES = {
|
||||||
|
0: "AccountCreateContract", 1: "TransferContract", 2: "TransferAssetContract",
|
||||||
|
3: "VoteAssetContract", 4: "VoteWitnessContract", 5: "WitnessCreateContract",
|
||||||
|
6: "AssetIssueContract", 8: "WitnessUpdateContract", 9: "ParticipateAssetIssueContract",
|
||||||
|
10: "AccountUpdateContract", 11: "FreezeBalanceContract", 12: "UnfreezeBalanceContract",
|
||||||
|
13: "WithdrawBalanceContract", 14: "UnfreezeAssetContract", 15: "UpdateAssetContract",
|
||||||
|
16: "ProposalCreateContract", 17: "ProposalApproveContract", 18: "ProposalDeleteContract",
|
||||||
|
19: "SetAccountIdContract", 20: "CustomContract", 30: "CreateSmartContract",
|
||||||
|
31: "TriggerSmartContract", 33: "UpdateSettingContract", 41: "ExchangeCreateContract",
|
||||||
|
42: "ExchangeInjectContract", 43: "ExchangeWithdrawContract", 44: "ExchangeTransactionContract",
|
||||||
|
45: "UpdateEnergyLimitContract", 46: "AccountPermissionUpdateContract", 48: "ClearABIContract",
|
||||||
|
49: "UpdateBrokerageContract", 51: "ShieldedTransferContract", 52: "MarketSellAssetContract",
|
||||||
|
53: "MarketCancelOrderContract", 54: "FreezeBalanceV2Contract", 55: "UnfreezeBalanceV2Contract",
|
||||||
|
56: "WithdrawExpireUnfreezeContract", 57: "DelegateResourceContract",
|
||||||
|
58: "UnDelegateResourceContract", 59: "CancelAllUnfreezeV2Contract",
|
||||||
|
};
|
||||||
|
// Contract types that hand control of the account or its resources to a
|
||||||
|
// third party — the overlay stresses these.
|
||||||
|
const DANGEROUS_TYPES = new Set([46, 57]);
|
||||||
|
|
||||||
|
const TRC20_SELECTORS = {
|
||||||
|
a9059cbb: { name: "transfer", args: ["to", "amount"] },
|
||||||
|
"095ea7b3": { name: "approve", args: ["spender", "amount"] },
|
||||||
|
"23b872dd": { name: "transferFrom", args: ["from", "to", "amount"] },
|
||||||
|
"39509351": { name: "increaseAllowance", args: ["spender", "amount"] },
|
||||||
|
};
|
||||||
|
const UINT256_MAX = (1n << 256n) - 1n;
|
||||||
|
|
||||||
|
module.exports = function makeTronDecode({ sha256, base58check }) {
|
||||||
|
const hexToBytes = (h) => {
|
||||||
|
const s = String(h || "").replace(/^0x/i, "");
|
||||||
|
if (!/^[0-9a-f]*$/i.test(s) || s.length % 2) throw new Error("raw_data_hex is not hex");
|
||||||
|
const out = new Uint8Array(s.length / 2);
|
||||||
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
const bytesToHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
||||||
|
|
||||||
|
// Split a message into { field, wire, value } records. Varints come back
|
||||||
|
// as BigInt, length-delimited fields as Uint8Array views.
|
||||||
|
function readFields(bytes) {
|
||||||
|
const out = [];
|
||||||
|
let off = 0;
|
||||||
|
const varint = () => {
|
||||||
|
let v = 0n, shift = 0n;
|
||||||
|
for (;;) {
|
||||||
|
if (off >= bytes.length) throw new Error("truncated varint");
|
||||||
|
const b = bytes[off++];
|
||||||
|
v |= BigInt(b & 0x7f) << shift;
|
||||||
|
if ((b & 0x80) === 0) return v;
|
||||||
|
shift += 7n;
|
||||||
|
if (shift > 70n) throw new Error("varint too long");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
while (off < bytes.length) {
|
||||||
|
const key = varint();
|
||||||
|
const field = Number(key >> 3n), wire = Number(key & 7n);
|
||||||
|
if (wire === 0) out.push({ field, wire, value: varint() });
|
||||||
|
else if (wire === 1) { if (off + 8 > bytes.length) throw new Error("truncated fixed64"); out.push({ field, wire, value: bytes.subarray(off, off + 8) }); off += 8; }
|
||||||
|
else if (wire === 2) { const n = Number(varint()); if (off + n > bytes.length) throw new Error("truncated bytes"); out.push({ field, wire, value: bytes.subarray(off, off + n) }); off += n; }
|
||||||
|
else if (wire === 5) { if (off + 4 > bytes.length) throw new Error("truncated fixed32"); out.push({ field, wire, value: bytes.subarray(off, off + 4) }); off += 4; }
|
||||||
|
else throw new Error("unsupported wire type " + wire);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const one = (fields, n) => fields.find((f) => f.field === n);
|
||||||
|
const bytesOf = (fields, n) => { const f = one(fields, n); return f && f.wire === 2 ? f.value : null; };
|
||||||
|
const numOf = (fields, n) => { const f = one(fields, n); return f && f.wire === 0 ? f.value : null; };
|
||||||
|
|
||||||
|
// 21-byte Tron address (0x41 || h20) → "T…" base58check. Anything else is
|
||||||
|
// returned as hex so the overlay never hides a malformed field.
|
||||||
|
function addr(b) {
|
||||||
|
if (!b) return null;
|
||||||
|
if (b.length === 21 && b[0] === 0x41) { try { return base58check.encodeCheck(b); } catch {} }
|
||||||
|
return "0x" + bytesToHex(b);
|
||||||
|
}
|
||||||
|
// EVM-style 32-byte word → Tron address (last 20 bytes, 0x41 prefix).
|
||||||
|
function wordToAddr(word) {
|
||||||
|
const out = new Uint8Array(21); out[0] = 0x41; out.set(word.subarray(12, 32), 1);
|
||||||
|
return addr(out);
|
||||||
|
}
|
||||||
|
const wordToBig = (word) => { let v = 0n; for (const x of word) v = (v << 8n) | BigInt(x); return v; };
|
||||||
|
|
||||||
|
function decodeTrc20(data) {
|
||||||
|
if (!data || data.length < 4) return null;
|
||||||
|
const sel = bytesToHex(data.subarray(0, 4));
|
||||||
|
const spec = TRC20_SELECTORS[sel];
|
||||||
|
if (!spec) return { selector: sel, name: null };
|
||||||
|
const words = [];
|
||||||
|
for (let i = 4; i + 32 <= data.length; i += 32) words.push(data.subarray(i, i + 32));
|
||||||
|
if (words.length < spec.args.length) return { selector: sel, name: spec.name, malformed: true };
|
||||||
|
const out = { selector: sel, name: spec.name };
|
||||||
|
spec.args.forEach((a, i) => { out[a] = a === "amount" ? wordToBig(words[i]) : wordToAddr(words[i]); });
|
||||||
|
if (out.amount != null) out.unlimited = out.amount >= (1n << 255n) || out.amount === UINT256_MAX;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeContract(fields) {
|
||||||
|
const typeNum = Number(numOf(fields, 1) ?? 0n);
|
||||||
|
const typeName = CONTRACT_TYPES[typeNum] || `Contract#${typeNum}`;
|
||||||
|
const any = bytesOf(fields, 2);
|
||||||
|
const c = { type: typeNum, typeName, dangerous: DANGEROUS_TYPES.has(typeNum), owner: null, permissionId: Number(numOf(fields, 5) ?? 0n) };
|
||||||
|
if (!any) return c;
|
||||||
|
const anyFields = readFields(any);
|
||||||
|
const value = bytesOf(anyFields, 2);
|
||||||
|
if (!value) return c;
|
||||||
|
const vf = readFields(value);
|
||||||
|
if (typeNum === 1) { // TransferContract
|
||||||
|
c.owner = addr(bytesOf(vf, 1)); c.to = addr(bytesOf(vf, 2)); c.amount = numOf(vf, 3) ?? 0n;
|
||||||
|
} else if (typeNum === 2) { // TransferAssetContract
|
||||||
|
const name = bytesOf(vf, 1);
|
||||||
|
c.assetName = name ? Buffer.from(name).toString("utf8") : null;
|
||||||
|
c.owner = addr(bytesOf(vf, 2)); c.to = addr(bytesOf(vf, 3)); c.amount = numOf(vf, 4) ?? 0n;
|
||||||
|
} else if (typeNum === 31) { // TriggerSmartContract
|
||||||
|
c.owner = addr(bytesOf(vf, 1)); c.contract = addr(bytesOf(vf, 2));
|
||||||
|
c.callValue = numOf(vf, 3) ?? 0n;
|
||||||
|
const data = bytesOf(vf, 4);
|
||||||
|
c.data = data ? bytesToHex(data) : "";
|
||||||
|
c.call = decodeTrc20(data);
|
||||||
|
} else {
|
||||||
|
c.owner = addr(bytesOf(vf, 1));
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full decode. Throws on malformed input so the caller refuses to sign.
|
||||||
|
function decodeRawData(rawDataHex) {
|
||||||
|
const bytes = hexToBytes(rawDataHex);
|
||||||
|
if (!bytes.length) throw new Error("raw_data_hex is empty");
|
||||||
|
const fields = readFields(bytes);
|
||||||
|
const contracts = fields.filter((f) => f.field === 11 && f.wire === 2).map((f) => decodeContract(readFields(f.value)));
|
||||||
|
if (!contracts.length) throw new Error("raw_data_hex carries no contract");
|
||||||
|
const memo = bytesOf(fields, 10);
|
||||||
|
return {
|
||||||
|
txid: bytesToHex(sha256(bytes)),
|
||||||
|
contracts,
|
||||||
|
expiration: Number(numOf(fields, 8) ?? 0n),
|
||||||
|
timestamp: Number(numOf(fields, 14) ?? 0n),
|
||||||
|
feeLimit: numOf(fields, 18) ?? null,
|
||||||
|
memo: memo ? Buffer.from(memo).toString("utf8") : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { decodeRawData, decodeTrc20, readFields, CONTRACT_TYPES };
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue