@integraledger/profile-facilitator
v0.2.0
Published
An x402 facilitator for the LCP profiles x402/exact/tron/lcp-trc20-memo and x402/exact/polkadot/lcp-assets-remark: verifies and settles payments whose signed transaction carries the ATR hash.
Readme
@integraledger/profile-facilitator
An x402 facilitator for two LCP profiles: x402/exact/tron/lcp-trc20-memo and
x402/exact/polkadot/lcp-assets-remark. It verifies and settles payments whose payer-signed transaction carries the
ATR hash, and it holds no key and pays no fee.
Documentation: connectors.integraledger.com/guides/facilitator
What it is
In x402, a facilitator is the role that verifies a payment and settles it on its network, answering the x402
facilitator API: GET /supported, POST /verify and POST /settle. x402 specifies neither Tron nor Polkadot. The
Legal Context Protocol (LCP) defines one transfer method on each, in which the payer's own signed transaction carries
the ATR hash (H): SHA-256 over the exact bytes of the Agentic Transaction Record (ATR), the agreement's record.
Paying is then agreeing to that exact record, and the proof is on chain.
| Profile | network | extra.assetTransferMethod | Where H rides |
|---|---|---|---|
| x402/exact/tron/lcp-trc20-memo | tron:<chain id in decimal> | lcp-trc20-memo | The transaction's raw_data.data: the 77 ASCII bytes lcp:sha256: followed by H, beside one TRC-20 transfer(payTo, amount). |
| x402/exact/polkadot/lcp-assets-remark | polkadot:68d56f15f85d3136970ec16946040bc1 (Polkadot Asset Hub) or polkadot:67f9723393ef76214df0118c34bbbd3d (Westend Asset Hub) | lcp-assets-remark | system.remark_with_event(R), where R is exactly lcp:sha256: followed by H in lowercase hex, batched with assets.transfer_keep_alive in one utility.batch_all. |
This package implements each profile's rule 5, the facilitator's duties. The profiles themselves are published with
@integraledger/lcp, in integra-protocol.
The facilitator is a pass-through. It checks the payment against the requirements it is given, asks a node to validate or simulate it, submits exactly the bytes the payer signed, deduplicates, and reports what the network shows.
Key concepts
- Agentic Transaction Record (ATR): the agreement's record, a JSON document the seller serves.
- ATR hash (H): SHA-256 over the ATR's exact bytes.
- Legal Context Protocol (LCP): the pattern these profiles implement: the payment carries H, so paying is agreeing to that exact record.
- Pairing: a payment protocol, scheme and rail combination. This facilitator serves two:
x402/exact/tron/lcp-trc20-memoandx402/exact/polkadot/lcp-assets-remark. - Binding: how H rides in a pairing's payment: here, the Tron memo and the Polkadot remark.
- Seller: the party serving the resource. It calls this facilitator from its x402 resource server.
- Facilitator: the x402 role that verifies and settles.
- Deduplication: each payment is settled at most once, keyed by its network and transaction id, in Postgres.
sequenceDiagram
participant R as Resource server (seller)
participant F as Profile facilitator
participant P as Postgres
participant N as Network node
R->>F: POST /verify
F->>N: read the permission, reference block and id, and simulate (Tron), or validate (Polkadot)
F-->>R: isValid, payer
R->>F: POST /settle
F->>P: read, then claim the transaction id
F->>N: broadcast or submit the signed bytes
loop every second until included or the wait ends
F->>N: read the receipt or the blocks
end
F->>P: store the answer
F-->>R: success, transaction, payerInstall
npm install @integraledger/profile-facilitatorIt needs Node.js >=26.10.0, Postgres (the tests run on Postgres 18), and an HTTP endpoint for each network you
serve: a Tron FullNode and Solidity node, or a Polkadot Asset Hub RPC node. It is ESM only.
Quickstart
Start the facilitator for Tron mainnet and read what it supports. The node URLs below are java-tron's default HTTP
ports on your own node; the facilitator calls them only when a payment arrives. DATABASE_URL is your Postgres
connection string.
import { serveProfileFacilitator } from "@integraledger/profile-facilitator";
const facilitator = await serveProfileFacilitator({
listen: "127.0.0.1:4020",
tron: [{ network: "tron:728126428", fullNode: "http://127.0.0.1:8090", solidityNode: "http://127.0.0.1:8091" }],
polkadot: [{ network: "polkadot:68d56f15f85d3136970ec16946040bc1", rpc: "http://127.0.0.1:9944" }],
store: { url: process.env.DATABASE_URL ?? "postgres://[email protected]:5432/postgres" },
settleWaitMs: 30_000,
});
const supported = await (await fetch("http://127.0.0.1:4020/supported")).json();
console.log(JSON.stringify(supported, null, 2));
await facilitator.close();{
"kinds": [
{
"x402Version": 2,
"scheme": "exact",
"network": "tron:728126428",
"extra": {
"assetTransferMethod": "lcp-trc20-memo"
}
},
{
"x402Version": 2,
"scheme": "exact",
"network": "polkadot:68d56f15f85d3136970ec16946040bc1",
"extra": {
"assetTransferMethod": "lcp-assets-remark"
}
}
],
"extensions": [],
"signers": {}
}Run it
The package exports one function that starts the server; it has no command-line entry. A service is a short module that reads its configuration and stops cleanly on a signal:
import { serveProfileFacilitator } from "@integraledger/profile-facilitator";
const listen = process.env.LISTEN ?? "127.0.0.1:4021";
const facilitator = await serveProfileFacilitator({
listen,
tron: [
{
network: "tron:728126428",
fullNode: process.env.TRON_FULL_NODE ?? "http://127.0.0.1:8090",
solidityNode: process.env.TRON_SOLIDITY_NODE ?? "http://127.0.0.1:8091",
},
],
store: { url: process.env.DATABASE_URL ?? "postgres://[email protected]:5432/postgres" },
settleWaitMs: Number(process.env.SETTLE_WAIT_MS ?? 30_000),
});
for (const signal of ["SIGINT", "SIGTERM"] as const) {
process.once(signal, () => {
void facilitator.close().then(() => process.exit(0));
});
}
console.log(`facilitator listening on ${listen}`);facilitator listening on 127.0.0.1:4021Run it with node facilitator.ts: Node.js 26 runs TypeScript directly. close() stops accepting requests, closes
open connections, disconnects from the Polkadot nodes and closes the Postgres pool.
Configuration
serveProfileFacilitator(config) takes a ProfileFacilitatorConfig:
| Field | Type | Meaning |
|---|---|---|
| listen | string | "host:port"; an IPv6 host may be written in brackets. |
| tron | { network, fullNode, solidityNode }[], optional | One entry per Tron network. network is tron:<chain id in decimal>. fullNode serves /wallet/… and solidityNode serves /walletsolidity/…; give each its base URL. |
| polkadot | { network, rpc }[], optional | One entry per network, polkadot:68d56f15f85d3136970ec16946040bc1 or polkadot:67f9723393ef76214df0118c34bbbd3d. rpc is an HTTP JSON-RPC endpoint that serves state_call and author_submitExtrinsic. |
| store | { url } | The Postgres connection string. Postgres holds the deduplication table only. |
| settleWaitMs | number | How long /settle waits for inclusion before it answers settlement_pending. A value that is not a positive finite number means 30 000. |
The facilitator serves only the networks it is configured with. A request for any other network is answered
invalid_network. At start it creates its table if it does not exist; a Polkadot node's runtime metadata is loaded on
the first payment for that network, and loaded again whenever the node's runtime version changes.
Verify and settle
Both POST /verify and POST /settle take the x402 facilitator request:
{x402Version: 2, paymentPayload, paymentRequirements}, with paymentPayload.x402Version 2 and
paymentRequirements.scheme "exact".
import { serveProfileFacilitator } from "@integraledger/profile-facilitator";
const facilitator = await serveProfileFacilitator({
listen: "127.0.0.1:4022",
tron: [{ network: "tron:728126428", fullNode: "http://127.0.0.1:8090", solidityNode: "http://127.0.0.1:8091" }],
store: { url: process.env.DATABASE_URL ?? "postgres://[email protected]:5432/postgres" },
settleWaitMs: 30_000,
});
const request = {
x402Version: 2,
paymentPayload: { x402Version: 2, payload: { transaction: "0a02" } },
paymentRequirements: {
scheme: "exact",
network: "tron:3448148188",
amount: "10000",
asset: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
payTo: "TCwX1UeSkVfu43HD5xNMmFqDR2Xgjdxihx",
maxTimeoutSeconds: 60,
extra: { assetTransferMethod: "lcp-trc20-memo" },
},
};
for (const path of ["/verify", "/settle"]) {
const res = await fetch(`http://127.0.0.1:4022${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(request),
});
console.log(path, res.status, JSON.stringify(await res.json()));
}
await facilitator.close();/verify 200 {"isValid":false,"invalidReason":"invalid_network"}
/settle 200 {"success":false,"errorReason":"invalid_network","transaction":"","network":"tron:3448148188"}What /verify checks on Tron
In order; the first failure is the answer.
- The network is configured, and the requirements are ones the profile admits (its rule 1), else
invalid_networkorinvalid_payment_requirements. payload.transactionis a serialized TronTransactionin hex that decodes to exactly oneTriggerSmartContractwhosedatais an LCPsha256string, elseinvalid_payload.assetandpayToare Tron addresses, elseinvalid_payment_requirements.- The contract called is
asset, and the call data is exactlytransfer(payTo, amount), elseinvalid_payload. - The transaction carries exactly one signature, else
unsupported_permission. That signature recovers to the transaction's owner, elseinvalid_payload. - The store does not hold the transaction id, else
invalid_transaction_state. expirationis in the future and no later than now plusmaxTimeoutSeconds, elseinvalid_payload.- Five reads of the FullNode, made together. The first refusal in this order is the answer, and a node that does
not answer gives
unexpected_verify_error:- The owner's permission.
/wallet/getaccountgives the owner permission, and the weight it gives the owner's key must be at least its threshold, elseunsupported_permission. An account with no owner permission set, or one the node does not hold, has the network's default: its own key, weight 1, threshold 1. So a multi-signature owner, or an owner whose permission does not hold the address's own key, is refused here rather than at broadcast. - TaPoS. The node accepts a transaction only when
ref_block_hashequals bytes 8–15 of the id of the latest block whose number's bytes 6–7 areref_block_bytes. The facilitator reads the head with/wallet/getnowblock, then that block with/wallet/getblock, and makes the same comparison, elseinvalid_transaction. - The id.
/wallet/gettransactionbyidmust not find the transaction in a block, and/wallet/gettransactionfrompendingmust not find it in the node's pending pool, elseinvalid_transaction_state. - The simulation.
/wallet/triggerconstantcontractsimulates the transfer without failure, elseinvalid_transaction.
- The owner's permission.
The answer is {isValid: true, payer}, with payer the owner in base58check.
What /settle does on Tron
- Repeats the checks of
/verifythat read neither the store nor the node (steps 1 to 5). - Reads the stored answer for the transaction id. A stored success means the payment is consumed: the answer is
invalid_transaction_statewith the transaction id, never a second success. Any other final answer is returned as it is. A claimed id, or a storedsettlement_pending, goes on to step 6 and broadcasts nothing. - For an id the store does not hold, repeats the expiration check and the node reads of
/verify. An id the node already holds is a consumed payment, answeredinvalid_transaction_statewith the id. - Claims the transaction id with one conditional insert, so concurrent settles of one payment broadcast once.
- Broadcasts the signed bytes with
/wallet/broadcasthex. ADUP_TRANSACTION_ERRORcounts as broadcast. - Reads the transaction's status every second with
@integraledger/lcp'stronStatus: the Solidity node'sgettransactioninfobyidfirst, then the FullNode's. Success requires receipt resultSUCCESSand aTransferlog emitted byasset. The value the call returned is not read: a TRC-20 token may return false fromtransferand still transfer, and USDT-TRC20 does. A receipt with another result, orSUCCESSwith no such log, is a failure only once the Solidity node, which serves solidified blocks alone, shows it. A transaction whose expiration has passed and that no node shows once the solidified head is two slots past it can never be included. Each failure is answeredinvalid_transaction_state. - Stores the answer and returns it. A success is returned only by the settle whose write stored it, so of two
concurrent settles of one payment, one answers success and the other
invalid_transaction_statewith the id. A success whose write fails is answeredsettlement_pendingwith the id.
Success is {success: true, transaction, network, payer}, where transaction is the transaction id in hex.
What /verify checks on Polkadot
- The network is configured, and the requirements are ones the profile admits, else
invalid_networkorinvalid_payment_requirements. payload.extrinsicandpayload.callare lowercase hex; the call is exactlyutility.batch_all([assets.transfer_keep_alive(asset, Id(payTo), amount), system.remark_with_event(R)])for thisasset,payToandamount, and R is exactly the 77 byteslcp:sha256:followed by H in lowercase hex, elseinvalid_payload.System.Remarkedcarries the BLAKE2b-256 of the remark's bytes as signed, so a remark that spells H any other way, such as with upper-case digits, is refused.- With the network's runtime metadata, the extrinsic decodes as a signed v4 extrinsic whose call is
payload.callbyte for byte, with a mortal era, elseinvalid_payload. The node must serve the chain the network names (its genesis hash), elseunexpected_verify_error. TaggedTransactionQueue_validate_transactionat the best block returnsOk, elseinvalid_transaction.
The answer is {isValid: true, payer}, with payer the signer in SS58.
What /settle does on Polkadot
- Repeats every check of
/verify. - Reads the stored answer for the extrinsic's BLAKE2b-256 hash. A stored success means the payment is consumed: the
repeat answers
invalid_transaction_statewith the success'stransaction, never a second success. Any other final answer is returned as it is. - Claims the hash, with the first block to scan: the block after the finalized head read before validation, never before the era's birth.
- Submits with
author_submitExtrinsic. A refusal that says the node has already seen the extrinsic or its nonce is read as possibly included, and the scan runs. - Scans every second, up to the last block that can include the extrinsic: the era's last block, and no later than
System.BlockHashCount+ 1 blocks after the era's birth. Each block's execution keeps only the hashes of theBlockHashCount+ 1 blocks before it, and the chain refuses an extrinsic whose birth block's hash is gone. The scan reads first the finalized blocks, then the blocks up to the head. In the block that holds the extrinsic, success needsSystem.ExtrinsicSuccess,System.Remarkedfor R's BLAKE2b-256 andAssets.Transferredforasset. A failure counts only in a finalized block. - A finalized block the node cannot give, its body or, for the block that holds the extrinsic, its events, is recorded as unread and skipped. The scan goes on to the blocks after it, so a success in a later block is still found, and the unread block is read again on each later pass and on each repeat. A block read that cannot start before the deadline, because the process's 4 block reads are all running, is recorded the same way. A block above the finalized head that cannot be read is skipped, since it is read again at finality.
- Once every block to the last is finalized without the extrinsic, and no block is unread, it can never be included:
invalid_transaction_state. - Stores the answer and returns it. A success is given only by the
/settlethat stores it. When it cannot be stored, the answer issettlement_pendingwith the extrinsic's hash, and a later/settlereads the chain again.
Success is {success: true, transaction, network, payer}, where transaction is <block hash>-<extrinsic index>.
While the extrinsic is broadcast but not yet in a block, the answer is settlement_pending with transaction set to
the extrinsic's hash.
Pending and repeated settles
When the wait ends first, /settle answers {success: false, errorReason: "settlement_pending", transaction} with the
transaction id or extrinsic hash; x402 requires that transaction not be empty. Call /settle again with the same
request: it reads the chain from where it stopped, never submits twice, and returns the final answer once there is one.
The facilitator keeps the payment's row until that final answer is written, however long that takes, so a repeat made
after the validity window has ended still reads the chain. On Tron a payment is answered success once: every later
/settle of it answers invalid_transaction_state with the transaction id, because x402's exact family requires that
a consumed payment produce a settlement failure, never a success. An answer with an empty transaction means nothing
was submitted. When the store cannot be read, the facilitator cannot know whether the payment was submitted, so it
answers settlement_pending with the id.
A payment whose success has been answered is consumed. On Polkadot, a repeated /settle for it answers
invalid_transaction_state with the success's transaction, never a second success, as x402's exact family
requires: "A consumed primitive MUST produce a settlement failure, never a success." Of two concurrent settles of
one payment, one answers success and the other that failure.
API reference
The package's entry point exports:
| Export | Kind | What it is |
|---|---|---|
| serveProfileFacilitator(config) | (c: ProfileFacilitatorConfig) => Promise<{ close(): Promise<void> }> | Opens the store, starts the HTTP server and resolves once it listens. |
| ProfileFacilitatorConfig | interface | The configuration above. |
| VerifyAnswer | type | {isValid: true, payer} or {isValid: false, invalidReason, payer?}. |
| SettleAnswer | type | {success: true, transaction, network, payer} or {success: false, errorReason, transaction, network}. |
HTTP API
| Request | Answer |
|---|---|
| GET /supported | 200 {kinds, extensions: [], signers: {}}, one kind per configured network. |
| POST /verify | 200 with a VerifyAnswer; 400 {isValid: false, invalidReason: "invalid_payload"} when the body is not JSON. |
| POST /settle | 200 with a SettleAnswer; 400 with errorReason invalid_payload when the body is not JSON. |
| A body over 64 KiB | 413, and the connection is closed. |
| Another method on a served path | 405 {error}. |
| Any other path | 404 {error}. |
The facilitator has no authentication of its own. Serve it on a private interface, or behind your own network controls, to the resource servers that use it.
Reasons
| Reason | /verify | /settle | Meaning |
|---|---|---|---|
| invalid_x402_version | yes | yes | x402Version is not 2 in the request or the payload. |
| unsupported_scheme | yes | yes | scheme is not "exact". |
| invalid_network | yes | yes | The network is not one this facilitator is configured with. |
| invalid_payment_requirements | yes | yes | The requirements are not ones the profile admits. |
| invalid_payload | yes | yes | The payload does not match the profile or the requirements, or its signature or expiration fails. |
| unsupported_permission | yes | yes | Tron: the transaction carries more than one signature, or the owner's permission does not accept its one signature (the key's weight is below the threshold). |
| invalid_transaction | yes | yes | The node refuses the transaction in simulation or validation. On Tron this includes a reference block (TaPoS) that does not match the node's. |
| unexpected_verify_error | yes | no | The node or the store could not be read. |
| unexpected_settle_error | no | yes | The node could not be read before submission, or refused the submission. |
| settlement_pending | no | yes | Submitted, and not yet final when the wait ended. |
| invalid_transaction_state | yes | yes | /settle: included and failed, or it can never be included. On Tron also a consumed payment: its success was already answered, or the node already holds its id. /verify (Tron): the node, in a block or its pending pool, or the store already holds the transaction id. On Polkadot, also a repeat of a payment whose success has been answered. |
unsupported_permission and invalid_transaction are this facilitator's, for a Tron owner permission that does not
accept the transaction's one signature and for a transaction the node refuses in simulation or validation. The
others are the x402 specification's.
Storage
Postgres holds one table, created at start:
CREATE TABLE IF NOT EXISTS settlement (
network text NOT NULL,
id text NOT NULL,
answer jsonb,
until timestamptz NOT NULL,
since bigint,
unread bigint[] NOT NULL DEFAULT '{}',
PRIMARY KEY (network, id)
);
CREATE INDEX IF NOT EXISTS settlement_final_until ON settlement (until)
WHERE answer IS NOT NULL AND answer->>'errorReason' IS DISTINCT FROM 'settlement_pending'- One row per settled payment, keyed by network and transaction id (Tron) or extrinsic hash (Polkadot).
answeris null while the row is claimed and the answer is not yet written. A final answer is never replaced; a pending one is replaced by what a later read of the chain shows.untilis the end of the payment's validity window: Tron'sexpiration, or, for Polkadot, the end of the last block that can include the extrinsic (the era's last, and no later thanSystem.BlockHashCount+ 1 blocks after its birth), counted from the best block at 12 seconds a block.- A row whose answer is final (
success: true, or a failure other thansettlement_pending) is kept for 24 hours afteruntil, so a repeat in that time reads the stored answer before anything is verified again. - A row whose answer is not final (claimed with no answer, or
settlement_pending) is kept, whatever its age, until a repeated/settlereads the chain and writes a final answer. A repeat therefore always finds the id of a payment that was submitted, and never answers with an emptytransactionfor it. - A sweep every minute drops the rows whose answer is final and whose
untilis more than 24 hours past. It reads them throughsettlement_final_until, an index that holds only rows with a final answer, so the rows it keeps add nothing to its work. sinceis, for Polkadot, the first block the scan has not yet read at finality.unreadis, for Polkadot, the finalized blocks belowsincethat the node could not give. None of them is shown to lack the extrinsic, so each is read again, and the answer that the extrinsic can never be included waits until the list is empty. A scan's write keeps every block of the list that the scan did not read, so concurrent repeats lose none. It holds at most one entry per block of the validity window.- The pool holds at most 10 connections. Acquiring a connection, each query and each statement are bounded by 5 seconds.
Bounds and logs
- Each node call has a timeout of at most 5 seconds and an answer of at most 4 MiB, except Polkadot's
chain_getBlock. Its bound is sized from the runtime'sSystem.BlockLength, read from the metadata: 4 MiB, plus 9/2 of the largest class limit (an extrinsic of n encoded bytes is at most 2n + 5 JSON characters), plus 7 timesmaxHeaderSize. For Polkadot Asset Hub's runtime 2005000 that is 28,504,064 bytes. At most 4 of these reads run at once in one process, so block answers take at most 114,016,256 bytes for that runtime. A read waits for a free slot until the scan's deadline; one that cannot start by then is recorded as unread, like a block the node cannot give, and read again later. Every other node call has no such limit. On Tron,/verifymakes six FullNode calls: the account, the two id reads, the simulation and the head read at once, then the reference block read. - A request body is at most 64 KiB. The server's request timeout is
settleWaitMsplus 60 seconds. - The table holds a row with a final answer until 24 hours after its validity window ends. A row with no final answer
stays until a repeated
/settlefor it gives one. Each such row is a payment that passed every check and was claimed for submission; it is one row of fixed columns, the sweep's index does not hold it, and each repeat reads the chain withinsettleWaitMs. On Tron, one repeat made once the latest solidified block is two slots past the expiration gives a final answer whenever the nodes answer its reads. - The facilitator writes one JSON line on standard error for what the operator should see and the answer does not
carry:
settlement-failed(with the events, or the receipt result and why the transfer does not count, that the chain shows),settlement-expired,answer-not-stored,block-unreadable(a finalized Polkadot block recorded as unread, with the node's reason) andsettlement-consumed(a repeated/settleof a Polkadot payment whose success has been answered).
Security model and guarantees
- No key, no fee. The facilitator signs nothing. It submits exactly the bytes the payer signed, and the payer pays every fee, as both profiles require.
- Settled once. Settlements are deduplicated by transaction id or extrinsic hash, atomically, for as long as the
answer is not final, and for at least 24 hours after the validity window ends. A payment is answered success once,
across every process that shares the store, to the
/settlewhose write stores it, on Tron and on Polkadot. On Tron, a transaction id the node already holds is refused. - Success only from the chain.
success: trueis given only when a node shows the transaction in a block with the profile's success conditions: on Tron, receipt resultSUCCESSand aTransferlog from the token. - What it does not check. It checks that the payment carries H in the profile's form, not which H the seller issued.
Its store claims a payment's id once, so the payment is submitted once. On Tron and on Polkadot, a success is answered
only by the settle whose write stored it, and a repeated
/settleof that payment answersinvalid_transaction_statewith the transaction. Each profile's rule 6 puts the remaining checks on the resource server: it accepts a payment only when the payment's H is one it issued for that request and has not seen claimed. The seller door'sclaimmakes both checks. It answers404 claim/unknownfor an H it holds no record of,409 claim/not-this-requestfor a payment of another request, and409 claim/in-progressor409 claim/paidonce the ATR's payment has been claimed. The facilitator checks the amount, asset and payee against the requirements it is given because x402 requires that of a facilitator, and carries no business or legal logic beyond that.
Test vectors and conformance
The tests check each profile's rule 5 against local node stubs, with the transactions of @integraledger/lcp's
shared vectors. The Tron stubs answer with Tron mainnet's recorded answers (test/fixtures/tron-mainnet-answers.json),
and the Polkadot stub with Polkadot Asset Hub's recorded runtime metadata
(test/fixtures/polkadot-asset-hub-2005000.json.gz); the Tron account, block and transaction reads follow
java-tron's HTTP answers and its signature and TaPoS checks. They cover two concurrent settles of one payment (one
submission), repeated settles while the node is down, a settle that finds its id claimed and unanswered, the end of
each validity window (Tron's expiration and Polkadot's mortal era, capped by System.BlockHashCount), and a
Polkadot remark that spells H with upper-case digits, refused before the node is asked. On Tron they also cover the
owner's permission, TaPoS, a transaction id the node (in a block or its pending pool) or the store already holds, a
consumed payment, and a SUCCESS receipt with and without the token's Transfer log, including the live
USDT-TRC20 receipt whose transfer returned false. On Polkadot they also cover a repeat of a consumed payment, a
block answer over 4 MiB and one over the System.BlockLength bound, and a block the node cannot give, before and
after the era's end. They need
INTEGRA_DATABASE_URL set to a Postgres database.
Requirements
- Node.js
>=26.10.0. - Postgres; the tests run on Postgres 18.
- For Tron, a FullNode and a Solidity node with java-tron's HTTP API: the FullNode serves
/wallet/getaccount,/wallet/getnowblock,/wallet/getblock,/wallet/gettransactionbyid,/wallet/gettransactionfrompending,/wallet/triggerconstantcontract,/wallet/broadcasthexand/wallet/gettransactioninfobyid. For Polkadot, an Asset Hub RPC node that serves JSON-RPC over HTTP.
Contributing
See the repository's README and CONTRIBUTING.md.
