@geeqdev/geeq-sdk 0.1.0 → 0.2.0
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/.idea/geeq_sdk.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/.nvmrc +1 -0
- package/CONTRIBUTING.md +20 -0
- package/dist/src/address/record-type.d.ts +10 -0
- package/dist/src/address/record-type.js +59 -0
- package/dist/src/address/record-type.js.map +1 -0
- package/dist/src/address/wallet-address.d.ts +34 -0
- package/dist/src/address/wallet-address.js +90 -0
- package/dist/src/address/wallet-address.js.map +1 -0
- package/dist/src/amount.d.ts +43 -0
- package/dist/src/amount.js +105 -0
- package/dist/src/amount.js.map +1 -0
- package/dist/src/assert.d.ts +12 -0
- package/dist/src/assert.js +24 -0
- package/dist/src/assert.js.map +1 -0
- package/dist/src/capnp/adminNetTx.capnp.d.ts +75 -0
- package/dist/src/capnp/adminNetTx.capnp.js +66 -0
- package/dist/src/capnp/adminNetTx.capnp.js.map +1 -0
- package/dist/src/capnp/appLayBlock.capnp.d.ts +26 -0
- package/dist/src/capnp/appLayBlock.capnp.js +24 -0
- package/dist/src/capnp/appLayBlock.capnp.js.map +1 -0
- package/dist/src/capnp/appLayLedger.capnp.d.ts +34 -0
- package/dist/src/capnp/appLayLedger.capnp.js +32 -0
- package/dist/src/capnp/appLayLedger.capnp.js.map +1 -0
- package/dist/src/capnp/appLayRec.capnp.d.ts +129 -0
- package/dist/src/capnp/appLayRec.capnp.js +156 -0
- package/dist/src/capnp/appLayRec.capnp.js.map +1 -0
- package/dist/src/capnp/chainStateView.capnp.d.ts +50 -0
- package/dist/src/capnp/chainStateView.capnp.js +48 -0
- package/dist/src/capnp/chainStateView.capnp.js.map +1 -0
- package/dist/src/capnp/coinAcctRec.capnp.d.ts +178 -0
- package/dist/src/capnp/coinAcctRec.capnp.js +189 -0
- package/dist/src/capnp/coinAcctRec.capnp.js.map +1 -0
- package/dist/src/capnp/genesisBlock.capnp.d.ts +61 -0
- package/dist/src/capnp/genesisBlock.capnp.js +55 -0
- package/dist/src/capnp/genesisBlock.capnp.js.map +1 -0
- package/dist/src/capnp/hubContentStmt.capnp.d.ts +63 -0
- package/dist/src/capnp/hubContentStmt.capnp.js +57 -0
- package/dist/src/capnp/hubContentStmt.capnp.js.map +1 -0
- package/dist/src/capnp/misc.capnp.d.ts +20 -0
- package/dist/src/capnp/misc.capnp.js +18 -0
- package/dist/src/capnp/misc.capnp.js.map +1 -0
- package/dist/src/capnp/netActorRec.capnp.d.ts +65 -0
- package/dist/src/capnp/netActorRec.capnp.js +65 -0
- package/dist/src/capnp/netActorRec.capnp.js.map +1 -0
- package/dist/src/capnp/nodeContentStmt.capnp.d.ts +67 -0
- package/dist/src/capnp/nodeContentStmt.capnp.js +61 -0
- package/dist/src/capnp/nodeContentStmt.capnp.js.map +1 -0
- package/dist/src/capnp/nodePayloadList.capnp.d.ts +28 -0
- package/dist/src/capnp/nodePayloadList.capnp.js +26 -0
- package/dist/src/capnp/nodePayloadList.capnp.js.map +1 -0
- package/dist/src/capnp/nodeTxList.capnp.d.ts +36 -0
- package/dist/src/capnp/nodeTxList.capnp.js +34 -0
- package/dist/src/capnp/nodeTxList.capnp.js.map +1 -0
- package/dist/src/capnp/paramAcctRec.capnp.d.ts +338 -0
- package/dist/src/capnp/paramAcctRec.capnp.js +669 -0
- package/dist/src/capnp/paramAcctRec.capnp.js.map +1 -0
- package/dist/src/capnp/payload.capnp.d.ts +237 -0
- package/dist/src/capnp/payload.capnp.js +260 -0
- package/dist/src/capnp/payload.capnp.js.map +1 -0
- package/dist/src/capnp/proxyAcctRec.capnp.d.ts +223 -0
- package/dist/src/capnp/proxyAcctRec.capnp.js +255 -0
- package/dist/src/capnp/proxyAcctRec.capnp.js.map +1 -0
- package/dist/src/capnp/relayContentStmt.capnp.d.ts +82 -0
- package/dist/src/capnp/relayContentStmt.capnp.js +85 -0
- package/dist/src/capnp/relayContentStmt.capnp.js.map +1 -0
- package/dist/src/capnp/sysAcctRec.capnp.d.ts +48 -0
- package/dist/src/capnp/sysAcctRec.capnp.js +46 -0
- package/dist/src/capnp/sysAcctRec.capnp.js.map +1 -0
- package/dist/src/capnp/userAppTx.capnp.d.ts +230 -0
- package/dist/src/capnp/userAppTx.capnp.js +270 -0
- package/dist/src/capnp/userAppTx.capnp.js.map +1 -0
- package/dist/src/capnp/userValTx.capnp.d.ts +1575 -0
- package/dist/src/capnp/userValTx.capnp.js +2004 -0
- package/dist/src/capnp/userValTx.capnp.js.map +1 -0
- package/dist/src/capnp/valLayBlock.capnp.d.ts +88 -0
- package/dist/src/capnp/valLayBlock.capnp.js +86 -0
- package/dist/src/capnp/valLayBlock.capnp.js.map +1 -0
- package/dist/src/capnp/valLayLedger.capnp.d.ts +85 -0
- package/dist/src/capnp/valLayLedger.capnp.js +75 -0
- package/dist/src/capnp/valLayLedger.capnp.js.map +1 -0
- package/dist/src/chain/geeq-chain.d.ts +474 -0
- package/dist/src/chain/geeq-chain.js +721 -0
- package/dist/src/chain/geeq-chain.js.map +1 -0
- package/dist/src/chain/geeq-node.d.ts +57 -0
- package/dist/src/chain/geeq-node.js +62 -0
- package/dist/src/chain/geeq-node.js.map +1 -0
- package/dist/src/chain/nonce-guard.d.ts +42 -0
- package/dist/src/chain/nonce-guard.js +68 -0
- package/dist/src/chain/nonce-guard.js.map +1 -0
- package/dist/src/chain/select.d.ts +40 -0
- package/dist/src/chain/select.js +114 -0
- package/dist/src/chain/select.js.map +1 -0
- package/dist/src/crypto/account-id.d.ts +35 -0
- package/dist/src/crypto/account-id.js +58 -0
- package/dist/src/crypto/account-id.js.map +1 -0
- package/dist/src/crypto/keypair.d.ts +24 -0
- package/dist/src/crypto/keypair.js +158 -0
- package/dist/src/crypto/keypair.js.map +1 -0
- package/dist/src/crypto/mnemonic.d.ts +34 -0
- package/dist/src/crypto/mnemonic.js +57 -0
- package/dist/src/crypto/mnemonic.js.map +1 -0
- package/dist/src/encoding/base64.d.ts +8 -0
- package/dist/src/encoding/base64.js +53 -0
- package/dist/src/encoding/base64.js.map +1 -0
- package/dist/src/encoding/hex.d.ts +2 -0
- package/dist/src/encoding/hex.js +35 -0
- package/dist/src/encoding/hex.js.map +1 -0
- package/dist/src/errors.d.ts +83 -0
- package/dist/src/errors.js +49 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +59 -0
- package/dist/src/index.js +53 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/ledger/coin-accounts.d.ts +42 -0
- package/dist/src/ledger/coin-accounts.js +37 -0
- package/dist/src/ledger/coin-accounts.js.map +1 -0
- package/dist/src/ledger/decode.d.ts +18 -0
- package/dist/src/ledger/decode.js +113 -0
- package/dist/src/ledger/decode.js.map +1 -0
- package/dist/src/ledger/geeq-ledger.d.ts +99 -0
- package/dist/src/ledger/geeq-ledger.js +125 -0
- package/dist/src/ledger/geeq-ledger.js.map +1 -0
- package/dist/src/ledger/net-actors.d.ts +65 -0
- package/dist/src/ledger/net-actors.js +53 -0
- package/dist/src/ledger/net-actors.js.map +1 -0
- package/dist/src/ledger/nft-records.d.ts +13 -0
- package/dist/src/ledger/nft-records.js +41 -0
- package/dist/src/ledger/nft-records.js.map +1 -0
- package/dist/src/ledger/params.d.ts +34 -0
- package/dist/src/ledger/params.js +71 -0
- package/dist/src/ledger/params.js.map +1 -0
- package/dist/src/ledger/proxy-accounts.d.ts +26 -0
- package/dist/src/ledger/proxy-accounts.js +27 -0
- package/dist/src/ledger/proxy-accounts.js.map +1 -0
- package/dist/src/net/discovery.d.ts +62 -0
- package/dist/src/net/discovery.js +136 -0
- package/dist/src/net/discovery.js.map +1 -0
- package/dist/src/net/explorer.d.ts +57 -0
- package/dist/src/net/explorer.js +64 -0
- package/dist/src/net/explorer.js.map +1 -0
- package/dist/src/net/read-ledger.d.ts +7 -0
- package/dist/src/net/read-ledger.js +39 -0
- package/dist/src/net/read-ledger.js.map +1 -0
- package/dist/src/net/routes.d.ts +49 -0
- package/dist/src/net/routes.js +70 -0
- package/dist/src/net/routes.js.map +1 -0
- package/dist/src/net/transport.d.ts +55 -0
- package/dist/src/net/transport.js +168 -0
- package/dist/src/net/transport.js.map +1 -0
- package/dist/src/net/websocket.d.ts +49 -0
- package/dist/src/net/websocket.js +54 -0
- package/dist/src/net/websocket.js.map +1 -0
- package/dist/src/runtime/bindings.d.ts +9 -0
- package/dist/src/runtime/bindings.js +27 -0
- package/dist/src/runtime/bindings.js.map +1 -0
- package/dist/src/runtime/init.d.ts +13 -0
- package/dist/src/runtime/init.js +40 -0
- package/dist/src/runtime/init.js.map +1 -0
- package/dist/src/runtime/wasm-loader.browser.d.ts +14 -0
- package/dist/src/runtime/wasm-loader.browser.js +20 -0
- package/dist/src/runtime/wasm-loader.browser.js.map +1 -0
- package/dist/src/runtime/wasm-loader.node.d.ts +7 -0
- package/dist/src/runtime/wasm-loader.node.js +14 -0
- package/dist/src/runtime/wasm-loader.node.js.map +1 -0
- package/dist/src/socket/geeq-socket.d.ts +232 -0
- package/dist/src/socket/geeq-socket.js +634 -0
- package/dist/src/socket/geeq-socket.js.map +1 -0
- package/dist/src/socket/tracked-tx.d.ts +148 -0
- package/dist/src/socket/tracked-tx.js +212 -0
- package/dist/src/socket/tracked-tx.js.map +1 -0
- package/dist/src/socket/tx-status.d.ts +69 -0
- package/dist/src/socket/tx-status.js +72 -0
- package/dist/src/socket/tx-status.js.map +1 -0
- package/dist/src/tx/attestation.d.ts +60 -0
- package/dist/src/tx/attestation.js +163 -0
- package/dist/src/tx/attestation.js.map +1 -0
- package/dist/src/tx/content.d.ts +27 -0
- package/dist/src/tx/content.js +62 -0
- package/dist/src/tx/content.js.map +1 -0
- package/dist/src/tx/encode.d.ts +60 -0
- package/dist/src/tx/encode.js +82 -0
- package/dist/src/tx/encode.js.map +1 -0
- package/dist/src/tx/expiry.d.ts +11 -0
- package/dist/src/tx/expiry.js +12 -0
- package/dist/src/tx/expiry.js.map +1 -0
- package/dist/src/tx/fee.d.ts +16 -0
- package/dist/src/tx/fee.js +29 -0
- package/dist/src/tx/fee.js.map +1 -0
- package/dist/src/tx/metadata.d.ts +39 -0
- package/dist/src/tx/metadata.js +66 -0
- package/dist/src/tx/metadata.js.map +1 -0
- package/dist/src/tx/nft.d.ts +16 -0
- package/dist/src/tx/nft.js +98 -0
- package/dist/src/tx/nft.js.map +1 -0
- package/dist/src/tx/proxy.d.ts +37 -0
- package/dist/src/tx/proxy.js +134 -0
- package/dist/src/tx/proxy.js.map +1 -0
- package/dist/src/tx/transfer.d.ts +31 -0
- package/dist/src/tx/transfer.js +123 -0
- package/dist/src/tx/transfer.js.map +1 -0
- package/dist/src/types.d.ts +448 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tests/account-id.test.d.ts +1 -0
- package/dist/tests/account-id.test.js +27 -0
- package/dist/tests/account-id.test.js.map +1 -0
- package/dist/tests/amount.test.d.ts +1 -0
- package/dist/tests/amount.test.js +113 -0
- package/dist/tests/amount.test.js.map +1 -0
- package/dist/tests/attestation.test.d.ts +1 -0
- package/dist/tests/attestation.test.js +182 -0
- package/dist/tests/attestation.test.js.map +1 -0
- package/dist/tests/chain.test.d.ts +1 -0
- package/dist/tests/chain.test.js +647 -0
- package/dist/tests/chain.test.js.map +1 -0
- package/dist/tests/content.test.d.ts +1 -0
- package/dist/tests/content.test.js +90 -0
- package/dist/tests/content.test.js.map +1 -0
- package/dist/tests/documented-api.test.d.ts +1 -0
- package/dist/tests/documented-api.test.js +229 -0
- package/dist/tests/documented-api.test.js.map +1 -0
- package/dist/tests/explorer.test.d.ts +1 -0
- package/dist/tests/explorer.test.js +95 -0
- package/dist/tests/explorer.test.js.map +1 -0
- package/dist/tests/fee.test.d.ts +1 -0
- package/dist/tests/fee.test.js +179 -0
- package/dist/tests/fee.test.js.map +1 -0
- package/dist/tests/fixtures/build-vll.d.ts +71 -0
- package/dist/tests/fixtures/build-vll.js +141 -0
- package/dist/tests/fixtures/build-vll.js.map +1 -0
- package/dist/tests/fixtures/fake-socket.d.ts +44 -0
- package/dist/tests/fixtures/fake-socket.js +93 -0
- package/dist/tests/fixtures/fake-socket.js.map +1 -0
- package/dist/tests/fixtures/fake-transport.d.ts +24 -0
- package/dist/tests/fixtures/fake-transport.js +30 -0
- package/dist/tests/fixtures/fake-transport.js.map +1 -0
- package/dist/tests/keypair.test.d.ts +1 -0
- package/dist/tests/keypair.test.js +163 -0
- package/dist/tests/keypair.test.js.map +1 -0
- package/dist/tests/ledger.test.d.ts +1 -0
- package/dist/tests/ledger.test.js +234 -0
- package/dist/tests/ledger.test.js.map +1 -0
- package/dist/tests/mnemonic.test.d.ts +1 -0
- package/dist/tests/mnemonic.test.js +97 -0
- package/dist/tests/mnemonic.test.js.map +1 -0
- package/dist/tests/nft-transfer.test.d.ts +1 -0
- package/dist/tests/nft-transfer.test.js +170 -0
- package/dist/tests/nft-transfer.test.js.map +1 -0
- package/dist/tests/nft.test.d.ts +1 -0
- package/dist/tests/nft.test.js +180 -0
- package/dist/tests/nft.test.js.map +1 -0
- package/dist/tests/offline.test.d.ts +1 -0
- package/dist/tests/offline.test.js +95 -0
- package/dist/tests/offline.test.js.map +1 -0
- package/dist/tests/proxy.test.d.ts +1 -0
- package/dist/tests/proxy.test.js +183 -0
- package/dist/tests/proxy.test.js.map +1 -0
- package/dist/tests/select.test.d.ts +1 -0
- package/dist/tests/select.test.js +200 -0
- package/dist/tests/select.test.js.map +1 -0
- package/dist/tests/socket.test.d.ts +1 -0
- package/dist/tests/socket.test.js +607 -0
- package/dist/tests/socket.test.js.map +1 -0
- package/dist/tests/transfer.test.d.ts +1 -0
- package/dist/tests/transfer.test.js +225 -0
- package/dist/tests/transfer.test.js.map +1 -0
- package/dist/tests/wallet-address.test.d.ts +1 -0
- package/dist/tests/wallet-address.test.js +101 -0
- package/dist/tests/wallet-address.test.js.map +1 -0
- package/dist/tests/wasm-loader.jest.d.ts +13 -0
- package/dist/tests/wasm-loader.jest.js +15 -0
- package/dist/tests/wasm-loader.jest.js.map +1 -0
- package/example/.env.example +4 -0
- package/example/README.md +304 -0
- package/example/USAGE.md +74 -0
- package/example/index.html +12 -0
- package/example/package-lock.json +1557 -0
- package/example/package.json +25 -0
- package/example/scripts/browser-check.mjs +1421 -0
- package/example/scripts/match-derivation.mjs +65 -0
- package/example/src/App.vue +138 -0
- package/example/src/components/AuthorityPanel.vue +277 -0
- package/example/src/components/ChainPanel.vue +267 -0
- package/example/src/components/ChainState.vue +59 -0
- package/example/src/components/CopyButton.vue +25 -0
- package/example/src/components/CreateAssetPanel.vue +313 -0
- package/example/src/components/DigitalAssetsPanel.vue +68 -0
- package/example/src/components/EvidencePanel.vue +396 -0
- package/example/src/components/ExplorerPanel.vue +119 -0
- package/example/src/components/KeypairPanel.vue +182 -0
- package/example/src/components/PhrasePanel.vue +105 -0
- package/example/src/components/SocketWarning.vue +18 -0
- package/example/src/components/StepGate.vue +24 -0
- package/example/src/components/TransferPanel.vue +416 -0
- package/example/src/components/TxDeadline.vue +19 -0
- package/example/src/components/TxFee.vue +31 -0
- package/example/src/components/TxStatus.vue +105 -0
- package/example/src/composables/useSocketConnection.ts +80 -0
- package/example/src/env.d.ts +12 -0
- package/example/src/main.ts +5 -0
- package/example/src/pennies.ts +7 -0
- package/example/src/style.css +149 -0
- package/example/tsconfig.json +19 -0
- package/example/vite.config.ts +26 -0
- package/jest.config.js +18 -0
- package/package.json +2 -6
- package/scripts/copy-wasm.mjs +21 -0
- package/scripts/smoke.mjs +382 -0
- package/src/address/record-type.ts +77 -0
- package/src/address/wallet-address.ts +105 -0
- package/src/amount.ts +143 -0
- package/src/assert.ts +30 -0
- package/src/capnp/README.md +15 -0
- package/src/capnp/adminNetTx.capnp.ts +66 -0
- package/src/capnp/appLayBlock.capnp.ts +26 -0
- package/src/capnp/appLayLedger.capnp.ts +35 -0
- package/src/capnp/appLayRec.capnp.ts +159 -0
- package/src/capnp/chainStateView.capnp.ts +49 -0
- package/src/capnp/coinAcctRec.capnp.ts +193 -0
- package/src/capnp/genesisBlock.capnp.ts +56 -0
- package/src/capnp/hubContentStmt.capnp.ts +59 -0
- package/src/capnp/misc.capnp.ts +19 -0
- package/src/capnp/netActorRec.capnp.ts +64 -0
- package/src/capnp/nodeContentStmt.capnp.ts +62 -0
- package/src/capnp/nodePayloadList.capnp.ts +28 -0
- package/src/capnp/nodeTxList.capnp.ts +37 -0
- package/src/capnp/paramAcctRec.capnp.ts +669 -0
- package/src/capnp/payload.capnp.ts +261 -0
- package/src/capnp/proxyAcctRec.capnp.ts +259 -0
- package/src/capnp/relayContentStmt.capnp.ts +85 -0
- package/src/capnp/sysAcctRec.capnp.ts +47 -0
- package/src/capnp/userAppTx.capnp.ts +269 -0
- package/src/capnp/userValTx.capnp.ts +1999 -0
- package/src/capnp/valLayBlock.capnp.ts +93 -0
- package/src/capnp/valLayLedger.capnp.ts +80 -0
- package/src/chain/geeq-chain.ts +1094 -0
- package/src/chain/geeq-node.ts +105 -0
- package/src/chain/nonce-guard.ts +73 -0
- package/src/chain/select.ts +149 -0
- package/src/crypto/account-id.ts +83 -0
- package/src/crypto/keypair.ts +221 -0
- package/src/crypto/mnemonic.ts +69 -0
- package/src/encoding/base64.ts +58 -0
- package/src/encoding/hex.ts +42 -0
- package/src/errors.ts +122 -0
- package/src/index.ts +195 -0
- package/src/ledger/coin-accounts.ts +73 -0
- package/src/ledger/decode.ts +147 -0
- package/src/ledger/geeq-ledger.ts +184 -0
- package/src/ledger/net-actors.ts +129 -0
- package/src/ledger/nft-records.ts +49 -0
- package/src/ledger/params.ts +82 -0
- package/src/ledger/proxy-accounts.ts +51 -0
- package/src/net/discovery.ts +231 -0
- package/src/net/explorer.ts +123 -0
- package/src/net/read-ledger.ts +58 -0
- package/src/net/routes.ts +79 -0
- package/src/net/transport.ts +221 -0
- package/src/net/websocket.ts +94 -0
- package/src/runtime/bindings.ts +38 -0
- package/src/runtime/init.ts +49 -0
- package/src/runtime/wasm-loader.browser.ts +22 -0
- package/src/runtime/wasm-loader.node.ts +16 -0
- package/src/socket/geeq-socket.ts +827 -0
- package/src/socket/tracked-tx.ts +319 -0
- package/src/socket/tx-status.ts +131 -0
- package/src/tx/attestation.ts +222 -0
- package/src/tx/content.ts +82 -0
- package/src/tx/encode.ts +107 -0
- package/src/tx/expiry.ts +11 -0
- package/src/tx/fee.ts +31 -0
- package/src/tx/metadata.ts +100 -0
- package/src/tx/nft.ts +108 -0
- package/src/tx/proxy.ts +156 -0
- package/src/tx/transfer.ts +162 -0
- package/src/types.ts +503 -0
- package/src/wasm/README.md +22 -0
- package/src/wasm/browser/geeq_core.d.ts +1224 -0
- package/src/wasm/browser/geeq_core.js +6014 -0
- package/src/wasm/browser/geeq_core_bg.wasm +0 -0
- package/src/wasm/browser/geeq_core_bg.wasm.d.ts +447 -0
- package/src/wasm/nodejs/geeq_core.d.ts +753 -0
- package/src/wasm/nodejs/geeq_core.js +6016 -0
- package/src/wasm/nodejs/geeq_core_bg.wasm +0 -0
- package/src/wasm/nodejs/geeq_core_bg.wasm.d.ts +447 -0
- package/src/wasm/nodejs/package.json +3 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +60 -0
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end browser check.
|
|
3
|
+
*
|
|
4
|
+
* Builds nothing and mocks nothing: it serves the production build, drives it in a
|
|
5
|
+
* real Chromium, and asserts the full path works — the ~2 MB WebAssembly engine
|
|
6
|
+
* loads over HTTP, a recovery phrase is generated, and a keypair and address
|
|
7
|
+
* are derived from it in the page.
|
|
8
|
+
*
|
|
9
|
+
* npm run build && node scripts/browser-check.mjs
|
|
10
|
+
*
|
|
11
|
+
* The server also stubs a Geeq node: it serves a synthetic validation layer ledger on
|
|
12
|
+
* `GET /api/blocks?type=vll` and accepts transactions on `POST /api/blocks/process`, so
|
|
13
|
+
* the whole path — read the ledger, derive chain state, sign, submit — is driven end to
|
|
14
|
+
* end without a live node. The ledger is built with the same Cap'n Proto schema the SDK
|
|
15
|
+
* decodes with, and submitted transactions are decoded back and asserted field by field.
|
|
16
|
+
*
|
|
17
|
+
* The node's socket is stubbed in the page with Playwright's routeWebSocket, which swaps
|
|
18
|
+
* the page's WebSocket for an in-page stand-in: the SDK's socket code runs unchanged
|
|
19
|
+
* against frames this script sends and records, and no network handshake happens. That
|
|
20
|
+
* also lets a step make the socket unreachable without a browser error for it. Status
|
|
21
|
+
* events are keyed by a refHash the check computes from the bytes the node received, the
|
|
22
|
+
* way the node computes it — so a transaction id the SDK got wrong would never settle.
|
|
23
|
+
*/
|
|
24
|
+
import { chromium } from 'playwright-core'
|
|
25
|
+
import { createServer } from 'node:http'
|
|
26
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
27
|
+
import { dirname, extname, join, normalize } from 'node:path'
|
|
28
|
+
import { fileURLToPath } from 'node:url'
|
|
29
|
+
import assert from 'node:assert/strict'
|
|
30
|
+
import { createRequire } from 'node:module'
|
|
31
|
+
import { loadEnv } from 'vite'
|
|
32
|
+
|
|
33
|
+
const root = join(dirname(dirname(fileURLToPath(import.meta.url))), 'dist')
|
|
34
|
+
const TYPES = {
|
|
35
|
+
'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',
|
|
36
|
+
'.wasm': 'application/wasm', '.map': 'application/json',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Resolved through the SDK so the fixture is built with exactly the schema it decodes.
|
|
40
|
+
const sdkRoot = dirname(createRequire(import.meta.url).resolve('@geeqdev/geeq-sdk/package.json'))
|
|
41
|
+
const sdkRequire = createRequire(join(sdkRoot, 'package.json'))
|
|
42
|
+
const { Message, Uint64 } = sdkRequire('capnp-ts')
|
|
43
|
+
const {
|
|
44
|
+
ValLayLedger,
|
|
45
|
+
AppLayLedger,
|
|
46
|
+
UserAppTx,
|
|
47
|
+
Payload,
|
|
48
|
+
UserValTx,
|
|
49
|
+
NetActorStatus,
|
|
50
|
+
NetActorType,
|
|
51
|
+
} = loadSchemas()
|
|
52
|
+
const { blake3 } = sdkRequire('@noble/hashes/blake3')
|
|
53
|
+
const wasm = sdkRequire('./src/wasm/nodejs/geeq_core.js')
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The SDK's vendored Cap'n Proto schemas (src/capnp), compiled for this script with the
|
|
57
|
+
* SDK's own esbuild. capnp-ts stays external so the classes share the `Message` above.
|
|
58
|
+
*/
|
|
59
|
+
function loadSchemas() {
|
|
60
|
+
const { buildSync } = sdkRequire('esbuild')
|
|
61
|
+
const { outputFiles } = buildSync({
|
|
62
|
+
stdin: {
|
|
63
|
+
contents: [
|
|
64
|
+
"export { ValLayLedger } from './valLayLedger.capnp'",
|
|
65
|
+
"export { AppLayLedger } from './appLayLedger.capnp'",
|
|
66
|
+
"export { UserAppTx } from './userAppTx.capnp'",
|
|
67
|
+
"export { Payload } from './payload.capnp'",
|
|
68
|
+
"export { UserValTx } from './userValTx.capnp'",
|
|
69
|
+
"export { NetActorStatus, NetActorType } from './netActorRec.capnp'",
|
|
70
|
+
].join('\n'),
|
|
71
|
+
resolveDir: join(sdkRoot, 'src', 'capnp'),
|
|
72
|
+
loader: 'ts',
|
|
73
|
+
},
|
|
74
|
+
bundle: true,
|
|
75
|
+
write: false,
|
|
76
|
+
format: 'cjs',
|
|
77
|
+
platform: 'node',
|
|
78
|
+
external: ['capnp-ts'],
|
|
79
|
+
logLevel: 'silent',
|
|
80
|
+
})
|
|
81
|
+
const module = { exports: {} }
|
|
82
|
+
new Function('require', 'module', 'exports', outputFiles[0].text)(sdkRequire, module, module.exports)
|
|
83
|
+
return module.exports
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The refHash a node files a posted transaction under, computed as the node does it
|
|
88
|
+
* (geeq_node_core wrappers): an application-layer transaction is hashed WITH its
|
|
89
|
+
* signature, a validation-layer one WITHOUT. Independent of the SDK on purpose.
|
|
90
|
+
*/
|
|
91
|
+
function nodeRefHash(posted) {
|
|
92
|
+
const packed = Buffer.from(posted.data.transaction, 'base64')
|
|
93
|
+
const handle =
|
|
94
|
+
posted.type === 'uat'
|
|
95
|
+
? wasm.UserAppTx.fromUserAppTxCapnp(packed)
|
|
96
|
+
: wasm.UserValTxData.fromUserValTxCapnp(packed)
|
|
97
|
+
const flat = handle.flatten()
|
|
98
|
+
handle.free()
|
|
99
|
+
return Buffer.from(blake3(flat, { dkLen: 32 })).toString('base64')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const geeqSdk = await import('@geeqdev/geeq-sdk')
|
|
103
|
+
|
|
104
|
+
// The demo's pennies-per-Geeq, read from .env as Vite read it for the build under test.
|
|
105
|
+
// Every figure here is written in geeq and converted with it, so the check holds whatever
|
|
106
|
+
// the setting is.
|
|
107
|
+
const penniesPerGeeq = BigInt(loadEnv('production', dirname(root), 'VITE_').VITE_PENNIES_PER_GEEQ)
|
|
108
|
+
const geeq = (figure) => geeqSdk.toPennies({ geeq: figure }, penniesPerGeeq)
|
|
109
|
+
const shown = (pennies) => geeqSdk.formatGeeq(pennies, penniesPerGeeq)
|
|
110
|
+
/** A shown figure, escaped to go inside a RegExp. */
|
|
111
|
+
const shownPattern = (pennies) => shown(pennies).replace('.', '\\.')
|
|
112
|
+
|
|
113
|
+
/** The stub ledger's geeqNorm, which the minimum transfer and every fee are multiples of. */
|
|
114
|
+
const GEEQ_NORM = 1n
|
|
115
|
+
/** minTxAmt x geeqNorm is 0.1 geeq, as on the live chain; the minimum is one penny more. */
|
|
116
|
+
const MIN_TX_AMT = geeq('0.1') / GEEQ_NORM
|
|
117
|
+
const MINIMUM = MIN_TX_AMT * GEEQ_NORM + 1n
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The fee a node charges for a posted transaction, in pennies, worked out as the node does it
|
|
121
|
+
* (geeq_node_core wrappers/base.ts, calculateTransactionFee): geeqNorm per byte of the
|
|
122
|
+
* flatten WITH the signature — plus the payload's, for a uat — and a quarter of a percent of
|
|
123
|
+
* the amount, capped at geeqNorm x 10^7. Independent of the SDK on purpose.
|
|
124
|
+
*/
|
|
125
|
+
function nodeFee(posted, amount) {
|
|
126
|
+
const handle =
|
|
127
|
+
posted.type === 'uat'
|
|
128
|
+
? wasm.UserAppTx.fromUserAppTxCapnp(Buffer.from(posted.data.transaction, 'base64'))
|
|
129
|
+
: wasm.UserValTx.fromUserValTxCapnp(Buffer.from(posted.data.transaction, 'base64'))
|
|
130
|
+
let bytes = handle.flatten().length
|
|
131
|
+
handle.free()
|
|
132
|
+
if (posted.type === 'uat') {
|
|
133
|
+
const payload = wasm.Payload.fromPayloadCapnp(Buffer.from(posted.data.payload, 'base64'))
|
|
134
|
+
bytes += payload.flatten().length
|
|
135
|
+
payload.free()
|
|
136
|
+
}
|
|
137
|
+
const cap = GEEQ_NORM * 10_000_000n
|
|
138
|
+
return BigInt(bytes) * GEEQ_NORM + (amount / 400n < cap ? amount / 400n : cap)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Driven by the checks below to exercise the failure paths from inside the browser.
|
|
142
|
+
const ledger = {
|
|
143
|
+
mode: 'ok',
|
|
144
|
+
block: 41,
|
|
145
|
+
nonce: 7,
|
|
146
|
+
accountId: new Uint8Array(32),
|
|
147
|
+
host: '127.0.0.1',
|
|
148
|
+
balance: geeq('1000'),
|
|
149
|
+
// The id of the account's authority account, once a block has confirmed one.
|
|
150
|
+
authorityId: null,
|
|
151
|
+
// The digital assets the account holds: base64 id, content hash and labels.
|
|
152
|
+
assets: [],
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A uint64 of pennies, little-endian, as the protocol writes amounts. */
|
|
156
|
+
function pennies(value) {
|
|
157
|
+
const view = new DataView(new ArrayBuffer(8))
|
|
158
|
+
view.setBigUint64(0, value, true)
|
|
159
|
+
return Uint64.fromDataView(view)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// What the stubbed directory lists. The SDK picks from this; the page never names a node.
|
|
163
|
+
const directory = [
|
|
164
|
+
{ ip: '10.9.9.9', type: 'listener', status: 'ready' },
|
|
165
|
+
{ ip: '127.0.0.1', type: 'active', status: 'ready' },
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
function buildLedger() {
|
|
169
|
+
const message = new Message()
|
|
170
|
+
const vll = message.initRoot(ValLayLedger)
|
|
171
|
+
vll.setChainNum(1)
|
|
172
|
+
vll.setBlockNum(ledger.block)
|
|
173
|
+
|
|
174
|
+
const actors = vll.initNetActorList().initNetActorRecs(2)
|
|
175
|
+
const self = actors.get(0)
|
|
176
|
+
self.setIpAddr(ledger.host)
|
|
177
|
+
self.setPortNum(2082)
|
|
178
|
+
self.setNodeNumSelf(3)
|
|
179
|
+
self.setNodeNumSponsor(99)
|
|
180
|
+
// A real ledger sets this 100-355 blocks ahead on promotion; the capnp zero would
|
|
181
|
+
// read as a node already rotating out, which the SDK refuses to select.
|
|
182
|
+
self.setBlockNumRemove(ledger.block + 200)
|
|
183
|
+
self.setNetActorType(
|
|
184
|
+
ledger.mode === 'listener'
|
|
185
|
+
? NetActorType.NET_ACTOR_TYPE_ANCHOR_NODE
|
|
186
|
+
: NetActorType.NET_ACTOR_TYPE_ANON_NODE,
|
|
187
|
+
)
|
|
188
|
+
self.setRecStatus(
|
|
189
|
+
ledger.mode === 'listener'
|
|
190
|
+
? NetActorStatus.NET_ACTOR_STATUS_LISTENING
|
|
191
|
+
: NetActorStatus.NET_ACTOR_STATUS_NODE,
|
|
192
|
+
)
|
|
193
|
+
self.initAcctNumVal(32)
|
|
194
|
+
self.getAcctNumVal().copyBuffer(new Uint8Array(32).fill(0xa0))
|
|
195
|
+
|
|
196
|
+
// A second processing node, so the UI has a real alternative to offer.
|
|
197
|
+
const peer = actors.get(1)
|
|
198
|
+
peer.setIpAddr('10.9.9.9')
|
|
199
|
+
peer.setPortNum(2082)
|
|
200
|
+
peer.setNodeNumSelf(4)
|
|
201
|
+
peer.setBlockNumRemove(ledger.block + 200)
|
|
202
|
+
peer.setNetActorType(NetActorType.NET_ACTOR_TYPE_ANON_NODE)
|
|
203
|
+
peer.setRecStatus(NetActorStatus.NET_ACTOR_STATUS_NODE)
|
|
204
|
+
peer.initAcctNumVal(32)
|
|
205
|
+
peer.getAcctNumVal().copyBuffer(new Uint8Array(32).fill(0xb0))
|
|
206
|
+
|
|
207
|
+
const params = vll.initParamAcctList().initParamAcctRecs(4)
|
|
208
|
+
params.get(0).initParamType().initMetadataVarNum().setCurrentValue(
|
|
209
|
+
ledger.mode === 'tight-metadata' ? 1 : 4,
|
|
210
|
+
)
|
|
211
|
+
params.get(1).initParamType().initMetadataVarBytes().setCurrentValue(64)
|
|
212
|
+
// A chain's minimum transfer is minTxAmt x geeqNorm + 1. Fees are multiples of geeqNorm too.
|
|
213
|
+
params.get(2).initParamType().initMinTxAmt().setCurrentValue(pennies(MIN_TX_AMT))
|
|
214
|
+
params.get(3).initParamType().initGeeqNorm().setCurrentValue(pennies(GEEQ_NORM))
|
|
215
|
+
|
|
216
|
+
const wanted = ledger.mode === 'no-account' ? 0 : 1
|
|
217
|
+
const accounts = vll.initCoinAcctRecs(wanted)
|
|
218
|
+
if (wanted === 1) {
|
|
219
|
+
const account = accounts.get(0)
|
|
220
|
+
account.initAcctNumVal(32)
|
|
221
|
+
account.getAcctNumVal().copyBuffer(ledger.accountId)
|
|
222
|
+
account.setAmtCoin(pennies(ledger.balance))
|
|
223
|
+
if (ledger.mode === 'hashlock') account.getRecType().initHl()
|
|
224
|
+
else {
|
|
225
|
+
const asset = account.getRecType().initAsset()
|
|
226
|
+
asset.setNonce(ledger.nonce)
|
|
227
|
+
const held = asset.initNftAssetSubgroups(ledger.assets.length)
|
|
228
|
+
ledger.assets.forEach(({ id }, index) => held.get(index).initAcctNumApp(32).copyBuffer(Buffer.from(id, 'base64')))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (ledger.authorityId !== null) {
|
|
233
|
+
const authority = vll.initProxyAcctRecs(1).get(0)
|
|
234
|
+
authority.initAcctNumVal(32)
|
|
235
|
+
authority.getAcctNumVal().copyBuffer(Buffer.from(ledger.authorityId, 'base64'))
|
|
236
|
+
authority.setAmtCoin(pennies(geeq('20')))
|
|
237
|
+
// The node starts every new authority account at nonce 1; only creating a digital
|
|
238
|
+
// asset through it moves it.
|
|
239
|
+
authority.setNonce(1)
|
|
240
|
+
const recType = authority.getRecType().initNftMint()
|
|
241
|
+
recType.initAcctNumAdminUser(32)
|
|
242
|
+
recType.getAcctNumAdminUser().copyBuffer(ledger.accountId)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return Buffer.from(message.toPackedArrayBuffer()).toString('base64')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The application layer ledger, where what each digital asset stands for is kept. */
|
|
249
|
+
function buildAppLedger() {
|
|
250
|
+
const message = new Message()
|
|
251
|
+
const all = message.initRoot(AppLayLedger)
|
|
252
|
+
all.setChainNum(1)
|
|
253
|
+
all.setBlockNum(ledger.block)
|
|
254
|
+
const records = all.initAppLayRecs(ledger.assets.length)
|
|
255
|
+
ledger.assets.forEach((asset, index) => {
|
|
256
|
+
const record = records.get(index)
|
|
257
|
+
record.initAcctNumApp(32).copyBuffer(Buffer.from(asset.id, 'base64'))
|
|
258
|
+
const data = record.getRecType().initNft().initAttestationSubgroup().initData()
|
|
259
|
+
data.initAttestHash(32).copyBuffer(asset.attestHash)
|
|
260
|
+
const labels = data.initMetadata(asset.metadata.length)
|
|
261
|
+
asset.metadata.forEach((label, position) => {
|
|
262
|
+
labels.get(position).setPosition(position + 1)
|
|
263
|
+
labels.get(position).initItem(Buffer.byteLength(label)).copyBuffer(Buffer.from(label))
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
return Buffer.from(message.toPackedArrayBuffer()).toString('base64')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function ledgerBody() {
|
|
270
|
+
if (ledger.mode === 'empty') return JSON.stringify({ result: '' })
|
|
271
|
+
if (ledger.mode === 'garbage') return JSON.stringify({ result: 'not base64!!' })
|
|
272
|
+
return JSON.stringify({ result: buildLedger() })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const submitted = []
|
|
276
|
+
let ledgerReads = 0
|
|
277
|
+
// The stubbed block explorer: what it has indexed, by hex refHash, and what it was asked.
|
|
278
|
+
const indexed = new Map()
|
|
279
|
+
const explorerAsked = []
|
|
280
|
+
// 'refuse' makes the next submission come back 400, as a node refusing it would.
|
|
281
|
+
let submitMode = 'ok'
|
|
282
|
+
|
|
283
|
+
const server = createServer(async (req, res) => {
|
|
284
|
+
// Stubbed node directory: which chains exist, and which nodes serve them.
|
|
285
|
+
if (req.method === 'GET' && (req.url ?? '').startsWith('/api/admin/simulation/chains')) {
|
|
286
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
287
|
+
// Two chains, so the page has to name one — exactly as a real simulator would.
|
|
288
|
+
res.end(
|
|
289
|
+
JSON.stringify({
|
|
290
|
+
result: [
|
|
291
|
+
{ id: 4, chainNumber: 7, name: 'another-chain' },
|
|
292
|
+
{ id: 9, chainNumber: 1, name: 'browser-check' },
|
|
293
|
+
],
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
if (req.method === 'GET' && (req.url ?? '').startsWith('/api/nodes')) {
|
|
299
|
+
// Nodes are listed per chain, keyed by the directory's own row id. Answering only
|
|
300
|
+
// for chain 1's id proves the SDK resolved the requested chain rather than the
|
|
301
|
+
// first one listed.
|
|
302
|
+
const systemId = new URL(req.url, base).searchParams.get('systemId')
|
|
303
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
304
|
+
res.end(
|
|
305
|
+
JSON.stringify({
|
|
306
|
+
result: (systemId === '9' ? directory : []).map((node) => ({
|
|
307
|
+
ip: node.ip,
|
|
308
|
+
port: 3000,
|
|
309
|
+
protocol: 'http',
|
|
310
|
+
systemId: 9,
|
|
311
|
+
publicKey: '',
|
|
312
|
+
type: node.type,
|
|
313
|
+
status: node.status,
|
|
314
|
+
})),
|
|
315
|
+
}),
|
|
316
|
+
)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Stubbed node: serve the application layer ledger, and the validation layer one.
|
|
321
|
+
if (req.method === 'GET' && (req.url ?? '').startsWith('/api/blocks?type=all')) {
|
|
322
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
323
|
+
res.end(JSON.stringify({ result: buildAppLedger() }))
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
if (req.method === 'GET' && (req.url ?? '').startsWith('/api/blocks?')) {
|
|
327
|
+
ledgerReads += 1
|
|
328
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
329
|
+
res.end(ledgerBody())
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Stubbed node: accept a uat and record it.
|
|
334
|
+
if (req.method === 'POST' && (req.url ?? '').startsWith('/api/blocks/process')) {
|
|
335
|
+
const chunks = []
|
|
336
|
+
for await (const chunk of req) chunks.push(chunk)
|
|
337
|
+
submitted.push(JSON.parse(Buffer.concat(chunks).toString()))
|
|
338
|
+
if (submitMode === 'refuse') {
|
|
339
|
+
submitMode = 'ok'
|
|
340
|
+
res.writeHead(400, { 'content-type': 'application/json' })
|
|
341
|
+
res.end(JSON.stringify({ details: 'Receive uat, reason: refused by the browser check' }))
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
345
|
+
res.end(JSON.stringify({ result: true }))
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Stubbed block explorer: answers as the real one does, including a 500 for a refHash it
|
|
350
|
+
// does not have.
|
|
351
|
+
if (req.method === 'GET' && (req.url ?? '').startsWith('/api/transactions/byRefHash?')) {
|
|
352
|
+
const refHash = new URL(req.url, 'http://stub').searchParams.get('refHash')
|
|
353
|
+
explorerAsked.push(refHash)
|
|
354
|
+
const tx = indexed.get(refHash)
|
|
355
|
+
res.writeHead(tx ? 200 : 500, { 'content-type': 'application/json' })
|
|
356
|
+
res.end(
|
|
357
|
+
JSON.stringify(
|
|
358
|
+
tx
|
|
359
|
+
? { result: tx }
|
|
360
|
+
: { error: 'internal manager error', details: ['Transaction does not exist'] },
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const rel = normalize(decodeURIComponent((req.url ?? '/').split('?')[0])).replace(/^(\.\.[/\\])+/, '')
|
|
367
|
+
let file = join(root, rel === '/' ? 'index.html' : rel)
|
|
368
|
+
try {
|
|
369
|
+
if ((await stat(file)).isDirectory()) file = join(file, 'index.html')
|
|
370
|
+
} catch {
|
|
371
|
+
file = join(root, 'index.html')
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const body = await readFile(file)
|
|
375
|
+
res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream' })
|
|
376
|
+
res.end(body)
|
|
377
|
+
} catch {
|
|
378
|
+
res.writeHead(404).end('not found')
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r))
|
|
382
|
+
const base = `http://127.0.0.1:${server.address().port}`
|
|
383
|
+
|
|
384
|
+
const executablePath = process.env.CHROME_PATH ?? '/usr/bin/google-chrome'
|
|
385
|
+
const browser = await chromium.launch({ executablePath, args: ['--no-sandbox'] })
|
|
386
|
+
const page = await browser.newPage()
|
|
387
|
+
|
|
388
|
+
const consoleErrors = []
|
|
389
|
+
// Console lines a step provokes on purpose, each allowed exactly once.
|
|
390
|
+
const expectedConsoleErrors = []
|
|
391
|
+
const failedRequests = []
|
|
392
|
+
page.on('console', (m) => { if (m.type() === 'error') consoleErrors.push(m.text()) })
|
|
393
|
+
page.on('pageerror', (e) => consoleErrors.push(String(e)))
|
|
394
|
+
page.on('requestfailed', (r) => failedRequests.push(`${r.url()} ${r.failure()?.errorText}`))
|
|
395
|
+
|
|
396
|
+
const wasmResponses = []
|
|
397
|
+
page.on('response', (r) => {
|
|
398
|
+
if (r.url().endsWith('.wasm')) wasmResponses.push({ url: r.url(), status: r.status() })
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
let n = 0
|
|
402
|
+
const ok = (label) => { n += 1; console.log(` ok ${label}`) }
|
|
403
|
+
|
|
404
|
+
// The node socket. Every connection the page opens lands here; the newest is the live one
|
|
405
|
+
// (reconnecting the chain in step 3 replaces it). Each frame the page sends is stamped
|
|
406
|
+
// with how many transactions had been posted when it arrived, so ordering is checkable.
|
|
407
|
+
//
|
|
408
|
+
// While `socketDown` is set, a connection is closed before it opens — what a page sees
|
|
409
|
+
// when the node's certificate is untrusted or the node is gone.
|
|
410
|
+
let socketDown = false
|
|
411
|
+
const sockets = []
|
|
412
|
+
await page.routeWebSocket(
|
|
413
|
+
(url) => url.pathname === '/ws/',
|
|
414
|
+
async (route) => {
|
|
415
|
+
if (socketDown) {
|
|
416
|
+
await route.close()
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
const connection = { url: route.url(), route, frames: [], pings: 0 }
|
|
420
|
+
sockets.push(connection)
|
|
421
|
+
route.onMessage((message) => {
|
|
422
|
+
if (message === 'ping') {
|
|
423
|
+
connection.pings += 1
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
connection.frames.push({ ...JSON.parse(String(message)), postedBefore: submitted.length })
|
|
427
|
+
})
|
|
428
|
+
},
|
|
429
|
+
)
|
|
430
|
+
const nodeSocket = () => sockets[sockets.length - 1]
|
|
431
|
+
const push = (event, payload) => nodeSocket().route.send(JSON.stringify({ event, payload }))
|
|
432
|
+
const uutStatus = (refHash, status, extra = {}) =>
|
|
433
|
+
push('uut_status', {
|
|
434
|
+
refHash,
|
|
435
|
+
publicKeys: [publicKeyB64],
|
|
436
|
+
blockNumber: 42,
|
|
437
|
+
vlbBlock: 43,
|
|
438
|
+
isValid: status === 'valid',
|
|
439
|
+
status,
|
|
440
|
+
error: status === 'valid' ? '' : `${status} (browser check)`,
|
|
441
|
+
type: 'outgoing',
|
|
442
|
+
createdAt: `Date:${new Date().toString()}`,
|
|
443
|
+
...extra,
|
|
444
|
+
})
|
|
445
|
+
/** Resolves once the live socket has been told to subscribe `key`. */
|
|
446
|
+
const subscribedTo = async (key) => {
|
|
447
|
+
for (let i = 0; i < 100; i += 1) {
|
|
448
|
+
const frame = nodeSocket()?.frames.find(
|
|
449
|
+
(f) => f.event === 'client' && f.payload.publicKeys.includes(key),
|
|
450
|
+
)
|
|
451
|
+
if (frame) return frame
|
|
452
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
453
|
+
}
|
|
454
|
+
throw new Error(`the page never subscribed ${key}`)
|
|
455
|
+
}
|
|
456
|
+
let publicKeyB64 = ''
|
|
457
|
+
|
|
458
|
+
await page.goto(base, { waitUntil: 'networkidle' })
|
|
459
|
+
|
|
460
|
+
// 1. The engine actually loaded in the browser.
|
|
461
|
+
await page.waitForSelector('.dot.ready', { timeout: 30_000 })
|
|
462
|
+
ok('WebAssembly engine loaded in-browser')
|
|
463
|
+
assert.ok(wasmResponses.length > 0, 'the .wasm was requested')
|
|
464
|
+
assert.equal(wasmResponses[0].status, 200)
|
|
465
|
+
ok(`engine served over HTTP (${wasmResponses[0].url.split('/').pop()})`)
|
|
466
|
+
|
|
467
|
+
// Steps 1–8 are each wrapped in a gate, in order; the explorer needs nothing and is not.
|
|
468
|
+
// A held step names the first unfinished step it builds on, and cannot be used.
|
|
469
|
+
const gate = (index) => page.locator('.gate').nth(index)
|
|
470
|
+
const heldBy = async (index) => {
|
|
471
|
+
const note = gate(index).locator('[data-testid=step-locked]')
|
|
472
|
+
return (await note.count()) === 0 ? null : (await note.textContent()).trim()
|
|
473
|
+
}
|
|
474
|
+
assert.equal(await page.locator('.gate').count(), 8, 'steps 1–8 are gated, the explorer is not')
|
|
475
|
+
assert.equal(await heldBy(0), null, 'step 1 is open from the start')
|
|
476
|
+
assert.match(await heldBy(1), /step 1/, 'step 2 waits for a phrase')
|
|
477
|
+
assert.match(await heldBy(2), /step 1/, 'step 3 names the first unfinished step, not its own')
|
|
478
|
+
assert.match(await heldBy(7), /step 1/, 'and so does step 8')
|
|
479
|
+
assert.equal(await gate(1).locator('[inert]').count(), 1, 'a held step cannot be used')
|
|
480
|
+
ok('later steps wait for the first unfinished one, and say which')
|
|
481
|
+
|
|
482
|
+
// 2. Generate a recovery phrase.
|
|
483
|
+
await page.getByRole('button', { name: 'Generate phrase' }).click()
|
|
484
|
+
await page.waitForSelector('.word')
|
|
485
|
+
const words = await page.locator('.word').allTextContents()
|
|
486
|
+
assert.equal(words.length, 12, 'twelve words rendered')
|
|
487
|
+
ok(`generated a 12-word phrase (${words[0].replace(/^\d+\s*/, '')} …)`)
|
|
488
|
+
assert.equal(await heldBy(1), null, 'a phrase opens step 2')
|
|
489
|
+
assert.match(await heldBy(2), /step 2/, 'step 3 now waits for a keypair')
|
|
490
|
+
ok('a phrase opens step 2, and step 3 waits on it')
|
|
491
|
+
|
|
492
|
+
// 3. Derive a keypair from it.
|
|
493
|
+
await page.getByRole('button', { name: 'Derive keypair' }).click()
|
|
494
|
+
await page.waitForSelector('.field', { timeout: 20_000 })
|
|
495
|
+
const values = await page.locator('.field .v').allTextContents()
|
|
496
|
+
const [address, publicKey, accountId] = values
|
|
497
|
+
|
|
498
|
+
assert.ok(address.length > 40, 'address looks like an address')
|
|
499
|
+
assert.match(publicKey, /^[A-Za-z0-9+/]{43}=$/, 'public key is 32 bytes of base64')
|
|
500
|
+
assert.match(accountId, /^[A-Za-z0-9+/]{43}=$/, 'account id is 32 bytes of base64')
|
|
501
|
+
ok(`derived address ${address.slice(0, 20)}…`)
|
|
502
|
+
ok('public key and account id rendered')
|
|
503
|
+
assert.equal(await heldBy(2), null, 'a keypair opens step 3')
|
|
504
|
+
assert.match(await heldBy(3), /step 3/, 'transactions wait for a node')
|
|
505
|
+
ok('a keypair opens step 3, and the transaction steps wait for a node')
|
|
506
|
+
|
|
507
|
+
// 4. The private key is masked until explicitly revealed.
|
|
508
|
+
assert.ok(values[3].startsWith('•'), 'private key masked by default')
|
|
509
|
+
ok('private key hidden until revealed')
|
|
510
|
+
|
|
511
|
+
// 5. Changing the chain re-derives the address.
|
|
512
|
+
await page.locator('#chain').fill('2')
|
|
513
|
+
await page.locator('#chain').dispatchEvent('input')
|
|
514
|
+
await page.waitForFunction(
|
|
515
|
+
(previous) => document.querySelector('.field .v')?.textContent !== previous,
|
|
516
|
+
address,
|
|
517
|
+
{ timeout: 5000 },
|
|
518
|
+
)
|
|
519
|
+
const chain2 = (await page.locator('.field .v').first().textContent()) ?? ''
|
|
520
|
+
assert.notEqual(chain2, address, 'address changes with the chain number')
|
|
521
|
+
ok('address re-derives when the chain number changes')
|
|
522
|
+
|
|
523
|
+
// 6. Restore mode validates a phrase live.
|
|
524
|
+
const phraseCard = page.locator('section.card').first()
|
|
525
|
+
await page.getByRole('button', { name: 'Restore existing' }).click()
|
|
526
|
+
// Leaving the phrase drops the keypair derived from it, so step 2 waits again.
|
|
527
|
+
assert.match(await heldBy(1), /step 1/, 'step 2 waits for a phrase again')
|
|
528
|
+
await phraseCard.locator('textarea').fill('not a real recovery phrase at all')
|
|
529
|
+
await page.waitForSelector('.invalid')
|
|
530
|
+
ok('rejects an invalid phrase')
|
|
531
|
+
await phraseCard.locator('textarea').fill(
|
|
532
|
+
'legal winner thank year wave sausage worth useful legal winner thank yellow',
|
|
533
|
+
)
|
|
534
|
+
await page.waitForSelector('.valid')
|
|
535
|
+
ok('accepts a valid phrase')
|
|
536
|
+
|
|
537
|
+
// 7. Evidence. Step 6 switched to restore mode, which clears the phrase and with
|
|
538
|
+
// it the keypair, so take the restored phrase through to a fresh key first.
|
|
539
|
+
await page.getByRole('button', { name: 'Use this phrase' }).click()
|
|
540
|
+
await page.getByRole('button', { name: 'Derive keypair' }).click()
|
|
541
|
+
await page.waitForSelector('.field', { timeout: 20_000 })
|
|
542
|
+
|
|
543
|
+
// The stub ledger must contain the account the page actually derived, so read it from
|
|
544
|
+
// the page rather than recomputing it and hoping the derivation options still match.
|
|
545
|
+
const accountIdB64 = (
|
|
546
|
+
await page
|
|
547
|
+
.locator('section.card')
|
|
548
|
+
.nth(1)
|
|
549
|
+
.locator('.field')
|
|
550
|
+
.filter({ hasText: 'Account id' })
|
|
551
|
+
.locator('.v')
|
|
552
|
+
.textContent()
|
|
553
|
+
).trim()
|
|
554
|
+
ledger.accountId = new Uint8Array(Buffer.from(accountIdB64, 'base64'))
|
|
555
|
+
assert.equal(ledger.accountId.length, 32, 'scraped a 32-byte account id from the page')
|
|
556
|
+
publicKeyB64 = (
|
|
557
|
+
await page
|
|
558
|
+
.locator('section.card')
|
|
559
|
+
.nth(1)
|
|
560
|
+
.locator('.field')
|
|
561
|
+
// Anchored: the account id's label mentions "the public key" too.
|
|
562
|
+
.filter({ hasText: /^Public key/ })
|
|
563
|
+
.locator('.v')
|
|
564
|
+
.textContent()
|
|
565
|
+
).trim()
|
|
566
|
+
|
|
567
|
+
const chainCard = page.locator('section.card').nth(2)
|
|
568
|
+
const evidenceCard = page.locator('section.card').nth(3)
|
|
569
|
+
const transferCard = page.locator('section.card').nth(4)
|
|
570
|
+
|
|
571
|
+
// The page never names a node: it names the directory, and the SDK picks an active one
|
|
572
|
+
// from it. The listener in the directory must not be chosen.
|
|
573
|
+
await chainCard.locator('[data-testid=simulator-url]').fill(base)
|
|
574
|
+
// The stub is not behind the standard TLS front door, so the harness override applies.
|
|
575
|
+
await chainCard.locator('[data-testid=dev-options]').click()
|
|
576
|
+
await chainCard.locator('[data-testid=node-port]').fill(String(new URL(base).port))
|
|
577
|
+
// Step 3 searches for the chain step 2 derived for, and only shows it: it is set there.
|
|
578
|
+
const shownChain = chainCard.locator('[data-testid=chain-number]')
|
|
579
|
+
assert.equal((await shownChain.textContent()).trim(), '2', 'step 5 left chain 2')
|
|
580
|
+
assert.equal(
|
|
581
|
+
await shownChain.evaluate((el) => el.matches('input, select, textarea, [contenteditable]')),
|
|
582
|
+
false,
|
|
583
|
+
'the chain number cannot be edited in step 3',
|
|
584
|
+
)
|
|
585
|
+
// A number the directory does not carry must say so, and say what it does carry — that
|
|
586
|
+
// is a real deployment mistake and the message is all that stands between it and a shrug.
|
|
587
|
+
await chainCard.locator('[data-testid=connect]').click()
|
|
588
|
+
await chainCard.locator('[data-testid=connect-error]').waitFor({ timeout: 20_000 })
|
|
589
|
+
const missing = await chainCard.locator('[data-testid=connect-error]').textContent()
|
|
590
|
+
assert.match(missing, /does not list chain 2/, 'names the chain it could not find')
|
|
591
|
+
assert.match(
|
|
592
|
+
missing,
|
|
593
|
+
/another-chain \(chain 7\), browser-check \(chain 1\)/,
|
|
594
|
+
'lists the chains it does have, with their numbers',
|
|
595
|
+
)
|
|
596
|
+
ok('an unknown chain number is reported with the available ones')
|
|
597
|
+
|
|
598
|
+
// A number the directory does carry, set where it belongs: in step 2.
|
|
599
|
+
await page.locator('#chain').fill('1')
|
|
600
|
+
assert.equal((await shownChain.textContent()).trim(), '1', 'step 3 follows step 2')
|
|
601
|
+
await chainCard.locator('[data-testid=connect]').click()
|
|
602
|
+
await chainCard.locator('[data-testid=selected-node]').waitFor({ timeout: 20_000 })
|
|
603
|
+
|
|
604
|
+
assert.match(
|
|
605
|
+
await chainCard.locator('[data-testid=selected-node] .v').textContent(),
|
|
606
|
+
/chain 1 \(browser-check\)/,
|
|
607
|
+
'shows the simulator\'s name for the chain it found',
|
|
608
|
+
)
|
|
609
|
+
ok('the node was found by chain number')
|
|
610
|
+
|
|
611
|
+
assert.equal(await heldBy(3), null, 'a node opens the transaction steps')
|
|
612
|
+
ok('a node opens the transaction steps')
|
|
613
|
+
|
|
614
|
+
// Moving step 2 to another chain lets the node go: it serves chain 1 only, so the steps
|
|
615
|
+
// after it wait again until a node is found for the chain step 2 now names.
|
|
616
|
+
await page.locator('#chain').fill('2')
|
|
617
|
+
await chainCard.locator('[data-testid=chain-changed]').waitFor({ timeout: 5_000 })
|
|
618
|
+
assert.equal(await chainCard.locator('[data-testid=selected-node]').count(), 0, 'the node is let go')
|
|
619
|
+
assert.match(await heldBy(3), /step 3/, 'the transaction steps wait again')
|
|
620
|
+
await page.locator('#chain').fill('1')
|
|
621
|
+
await chainCard.locator('[data-testid=connect]').click()
|
|
622
|
+
await chainCard.locator('[data-testid=selected-node]').waitFor({ timeout: 20_000 })
|
|
623
|
+
assert.equal(await chainCard.locator('[data-testid=chain-changed]').count(), 0, 'the notice clears')
|
|
624
|
+
assert.equal(await heldBy(3), null, 'and a new node opens them again')
|
|
625
|
+
ok('an address moved to another chain lets its node go until one is found again')
|
|
626
|
+
|
|
627
|
+
const selected = await chainCard.locator('[data-testid=selected-node] .v').textContent()
|
|
628
|
+
assert.match(selected, /127\.0\.0\.1/, 'picked the active node from the directory')
|
|
629
|
+
assert.doesNotMatch(selected, /10\.9\.9\.9/, 'did not pick the listener')
|
|
630
|
+
assert.match(selected, /chain 1/, 'picked the chain the page named, not the first listed')
|
|
631
|
+
assert.match(selected, /2 listed, 1 active/, 'reported what it chose from')
|
|
632
|
+
ok('the SDK chose an active node from the directory')
|
|
633
|
+
|
|
634
|
+
// The node's socket opens alongside the chain, on the node's own origin at /ws/.
|
|
635
|
+
await chainCard.locator('[data-testid=socket][data-state=open]').waitFor({ timeout: 10_000 })
|
|
636
|
+
assert.equal(nodeSocket().url, `ws://127.0.0.1:${new URL(base).port}/ws/`, 'socket URL derived from the node')
|
|
637
|
+
ok('the node socket opened on the chosen node, at /ws/')
|
|
638
|
+
|
|
639
|
+
// The account is subscribed as soon as there is an account and a socket, under
|
|
640
|
+
// both the forms a node files events by.
|
|
641
|
+
const accountFrame = await subscribedTo(accountIdB64)
|
|
642
|
+
assert.ok(accountFrame.payload.publicKeys.includes(publicKeyB64), 'and under its public key')
|
|
643
|
+
ok('the account is subscribed by public key and account id')
|
|
644
|
+
|
|
645
|
+
// Nothing has been read from the chain itself yet: the ledger is only fetched when a
|
|
646
|
+
// transaction is actually signed, because its values go stale within a few blocks.
|
|
647
|
+
assert.equal(ledgerReads, 0, 'connecting reads no ledger')
|
|
648
|
+
ok('no ledger read before one is needed')
|
|
649
|
+
|
|
650
|
+
// BLAKE3-32 of "hello" — the published test vector, so this pins our hashing.
|
|
651
|
+
const helloHash = 'ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f'
|
|
652
|
+
await evidenceCard.getByRole('button', { name: 'File', exact: true }).click()
|
|
653
|
+
await evidenceCard
|
|
654
|
+
.locator('input[type=file]')
|
|
655
|
+
.setInputFiles({ name: 'hello.txt', mimeType: 'text/plain', buffer: Buffer.from('hello') })
|
|
656
|
+
await evidenceCard.locator('[data-testid=evidence-hash]', { hasText: helloHash }).waitFor({ timeout: 5_000 })
|
|
657
|
+
ok('evidence hash is BLAKE3-32 of the file')
|
|
658
|
+
|
|
659
|
+
// A pasted hash is used as given; anything that is not 32 bytes of hex is refused.
|
|
660
|
+
await evidenceCard.getByRole('button', { name: 'Paste hash', exact: true }).click()
|
|
661
|
+
const hashInput = evidenceCard.locator('[data-testid=evidence-hash-input]')
|
|
662
|
+
await hashInput.fill(helloHash.slice(2))
|
|
663
|
+
await evidenceCard.locator('[data-testid=evidence-hash-error]').waitFor({ timeout: 5_000 })
|
|
664
|
+
assert.equal(await evidenceCard.locator('[data-testid=evidence-hash]').count(), 0, 'a short hash is not used')
|
|
665
|
+
await hashInput.fill(`0x${helloHash.toUpperCase()}`)
|
|
666
|
+
await evidenceCard.locator('[data-testid=evidence-hash]', { hasText: helloHash }).waitFor({ timeout: 5_000 })
|
|
667
|
+
ok('a pasted evidence hash is used, and a short one refused')
|
|
668
|
+
|
|
669
|
+
// Each later build pastes a hash of its own, so it never repeats the one before.
|
|
670
|
+
const pasteHash = (label) => hashInput.fill(Buffer.from(blake3(Buffer.from(label))).toString('hex'))
|
|
671
|
+
|
|
672
|
+
await evidenceCard.locator('#meta').fill('invoice-2026-09, acme-corp')
|
|
673
|
+
await evidenceCard.getByRole('button', { name: 'Build & sign' }).click()
|
|
674
|
+
await evidenceCard.locator('[data-testid=chain-state]').waitFor({ timeout: 20_000 })
|
|
675
|
+
assert.equal(ledgerReads, 1, 'the ledger is read once, while signing')
|
|
676
|
+
ok('the ledger is read at build time')
|
|
677
|
+
|
|
678
|
+
const cell = async (label) =>
|
|
679
|
+
(
|
|
680
|
+
await evidenceCard
|
|
681
|
+
.locator('[data-testid=chain-state] tr')
|
|
682
|
+
.filter({ hasText: label })
|
|
683
|
+
.locator('td')
|
|
684
|
+
.first()
|
|
685
|
+
.textContent()
|
|
686
|
+
).trim()
|
|
687
|
+
|
|
688
|
+
assert.equal(await cell('Chain'), '1', 'chain number read from the ledger')
|
|
689
|
+
assert.equal(await cell('Ledger block'), '41', 'ledger block read from the ledger')
|
|
690
|
+
assert.equal(await cell('Target block'), '42', 'target block is ledger block + 1')
|
|
691
|
+
assert.equal(await cell('Node number'), '3', 'node number is nodeNumSelf, not the index')
|
|
692
|
+
assert.equal(await cell('Nonce'), '7', 'nonce read from the coin-asset record')
|
|
693
|
+
ok('chain state derived from the ledger with no user input')
|
|
694
|
+
|
|
695
|
+
// The limits live in the ledger, so they become visible once one has been read.
|
|
696
|
+
assert.match(
|
|
697
|
+
await evidenceCard.locator('[data-testid=meta-limits]').textContent(),
|
|
698
|
+
/4 labels · 64 bytes each/,
|
|
699
|
+
'metadata limits read from the ledger parameters',
|
|
700
|
+
)
|
|
701
|
+
ok('metadata limits surfaced from the ledger')
|
|
702
|
+
|
|
703
|
+
await evidenceCard.locator('[data-testid=tx-base64]').waitFor({ timeout: 20_000 })
|
|
704
|
+
ok('built and signed evidence from a keypair and a hash alone')
|
|
705
|
+
|
|
706
|
+
// Round-tripping through base64 must reproduce the string exactly — Buffer.from
|
|
707
|
+
// silently ignores invalid characters, so decoding alone would prove nothing.
|
|
708
|
+
const readFields = async () => ({
|
|
709
|
+
tx: (await evidenceCard.locator('[data-testid=tx-base64]').textContent()).trim(),
|
|
710
|
+
payload: (await evidenceCard.locator('[data-testid=payload-base64]').textContent()).trim(),
|
|
711
|
+
})
|
|
712
|
+
const built = await readFields()
|
|
713
|
+
for (const [label, value] of Object.entries(built)) {
|
|
714
|
+
assert.ok(value.length > 40, `${label} is base64 of some length`)
|
|
715
|
+
assert.equal(Buffer.from(value, 'base64').toString('base64'), value, `${label} is valid base64`)
|
|
716
|
+
}
|
|
717
|
+
ok('transaction and payload are well-formed base64')
|
|
718
|
+
|
|
719
|
+
await evidenceCard.locator('[data-testid=submit]').click()
|
|
720
|
+
await evidenceCard.getByText('Node response', { exact: true }).waitFor({ timeout: 15_000 })
|
|
721
|
+
assert.equal(submitted.length, 1, 'exactly one transaction reached the node')
|
|
722
|
+
assert.equal(submitted[0].type, 'uat')
|
|
723
|
+
assert.equal(submitted[0].data.transaction, built.tx, 'posted transaction matches the built one')
|
|
724
|
+
assert.equal(submitted[0].data.payload, built.payload, 'posted payload matches the built one')
|
|
725
|
+
ok('posted exactly the transaction and payload that were signed')
|
|
726
|
+
|
|
727
|
+
// Close the loop: the values the fixture published must be the values actually signed
|
|
728
|
+
// into the bytes the node received.
|
|
729
|
+
const posted = new Message(
|
|
730
|
+
Buffer.from(submitted[0].data.transaction, 'base64'),
|
|
731
|
+
true,
|
|
732
|
+
).getRoot(UserAppTx).getData()
|
|
733
|
+
assert.equal(posted.getChainNum(), 1, 'signed chain number')
|
|
734
|
+
assert.equal(posted.getBlockNum(), 42, 'signed target block')
|
|
735
|
+
assert.equal(posted.getNodeNum(), 3, 'signed node number')
|
|
736
|
+
assert.equal(posted.getNonce(), 7, 'signed nonce')
|
|
737
|
+
ok('the derived values are the ones carried in the signed transaction')
|
|
738
|
+
|
|
739
|
+
// Subscribe before submit: the node had been told to push this account's statuses before
|
|
740
|
+
// the transaction reached it.
|
|
741
|
+
const beforePost = nodeSocket().frames.filter(
|
|
742
|
+
(f) => f.event === 'client' && f.postedBefore === 0 && f.payload.publicKeys.includes(accountIdB64),
|
|
743
|
+
)
|
|
744
|
+
assert.ok(beforePost.length > 0, 'the signer was subscribed before the POST')
|
|
745
|
+
ok('the signer was subscribed before the transaction was posted')
|
|
746
|
+
|
|
747
|
+
await evidenceCard.locator('[data-testid=tx-status][data-status=pending]').waitFor({ timeout: 5_000 })
|
|
748
|
+
ok('a 200 shows as pending, not as a success')
|
|
749
|
+
|
|
750
|
+
// The id the page shows is the refHash the node would file this transaction under.
|
|
751
|
+
const evidenceId = nodeRefHash(submitted[0])
|
|
752
|
+
assert.equal(
|
|
753
|
+
(await evidenceCard.locator('[data-testid=tx-id]').textContent()).trim(),
|
|
754
|
+
evidenceId,
|
|
755
|
+
'the SDK hashes evidence the way the node does — signature included',
|
|
756
|
+
)
|
|
757
|
+
ok('the evidence id matches the node refHash for the posted bytes')
|
|
758
|
+
|
|
759
|
+
uutStatus(evidenceId, 'valid')
|
|
760
|
+
await evidenceCard.locator('[data-testid=tx-status][data-status=confirmed]').waitFor({ timeout: 5_000 })
|
|
761
|
+
ok('a valid status over the socket confirms the evidence')
|
|
762
|
+
assert.match(
|
|
763
|
+
await chainCard.locator('[data-testid=socket-events]').textContent(),
|
|
764
|
+
/uut_status/,
|
|
765
|
+
'the socket panel shows what the node pushed',
|
|
766
|
+
)
|
|
767
|
+
ok('the raw node event is visible in the socket feed')
|
|
768
|
+
|
|
769
|
+
// Two transactions from one account against one block are BOTH rejected by the chain,
|
|
770
|
+
// so the SDK refuses to build the second rather than destroying the first.
|
|
771
|
+
await pasteHash('hello again')
|
|
772
|
+
await evidenceCard.getByRole('button', { name: 'Build & sign' }).click()
|
|
773
|
+
await evidenceCard.locator('[data-testid=build-error]').waitFor({ timeout: 20_000 })
|
|
774
|
+
assert.match(
|
|
775
|
+
await evidenceCard.locator('[data-testid=build-error]').textContent(),
|
|
776
|
+
/One transaction per account per block/,
|
|
777
|
+
'nonce collision is refused, not silently built',
|
|
778
|
+
)
|
|
779
|
+
ok('refuses a second evidence submission against the same block')
|
|
780
|
+
|
|
781
|
+
// Once the chain moves on, the same account can submit evidence again.
|
|
782
|
+
ledger.block = 42
|
|
783
|
+
ledger.nonce = 8
|
|
784
|
+
await evidenceCard.getByRole('button', { name: 'Build & sign' }).click()
|
|
785
|
+
await evidenceCard.locator('[data-testid=tx-base64]').waitFor({ timeout: 20_000 })
|
|
786
|
+
const afterAdvance = await readFields()
|
|
787
|
+
assert.notEqual(afterAdvance.tx, built.tx, 'a new block and nonce produce a new transaction')
|
|
788
|
+
ok('builds again once the block advances')
|
|
789
|
+
|
|
790
|
+
// A node answers a submission it refuses with an error and never sends a status for it,
|
|
791
|
+
// so the page must settle it from the response rather than wait for one.
|
|
792
|
+
submitMode = 'refuse'
|
|
793
|
+
// Chromium logs every 4xx response to the console; this one is the point of the step.
|
|
794
|
+
expectedConsoleErrors.push(/status of 400 \(Bad Request\)/)
|
|
795
|
+
await evidenceCard.locator('[data-testid=submit]').click()
|
|
796
|
+
await evidenceCard
|
|
797
|
+
.locator('[data-testid=tx-status][data-status=rejected][data-reason=refused]')
|
|
798
|
+
.waitFor({ timeout: 5_000 })
|
|
799
|
+
assert.match(
|
|
800
|
+
await evidenceCard.locator('[data-testid=tx-status]').textContent(),
|
|
801
|
+
/Refused on submission[\s\S]*refused by the browser check/,
|
|
802
|
+
'a refused submission is rejected at once, with the node words',
|
|
803
|
+
)
|
|
804
|
+
assert.equal(
|
|
805
|
+
await evidenceCard.locator('[data-testid=tx-events] li').count(),
|
|
806
|
+
0,
|
|
807
|
+
'settled from the response alone — no socket event was sent for it',
|
|
808
|
+
)
|
|
809
|
+
ok('a refused submission is rejected without waiting on the socket')
|
|
810
|
+
|
|
811
|
+
// An unfunded account is the ordinary first experience, and gets its own explanation.
|
|
812
|
+
ledger.mode = 'no-account'
|
|
813
|
+
await pasteHash('unfunded')
|
|
814
|
+
await evidenceCard.getByRole('button', { name: 'Build & sign' }).click()
|
|
815
|
+
await evidenceCard.locator('[data-testid=build-error]').waitFor({ timeout: 20_000 })
|
|
816
|
+
assert.match(
|
|
817
|
+
await evidenceCard.locator('[data-testid=build-error]').textContent(),
|
|
818
|
+
/never received coins/,
|
|
819
|
+
'an unfunded account is explained, not reported as a failure',
|
|
820
|
+
)
|
|
821
|
+
ok('an account missing from the ledger is explained')
|
|
822
|
+
|
|
823
|
+
// A node that answers but cannot be decoded must not look like a network problem.
|
|
824
|
+
ledger.mode = 'garbage'
|
|
825
|
+
// A fresh chain, so the decode is attempted rather than served from the cache the
|
|
826
|
+
// previous builds filled.
|
|
827
|
+
await chainCard.locator('[data-testid=connect]').click()
|
|
828
|
+
await chainCard.locator('[data-testid=selected-node]').waitFor({ timeout: 20_000 })
|
|
829
|
+
await pasteHash('garbage ledger')
|
|
830
|
+
await evidenceCard.getByRole('button', { name: 'Build & sign' }).click()
|
|
831
|
+
await page.waitForFunction(
|
|
832
|
+
() => document.querySelector('[data-testid=build-error]')?.textContent?.includes('base64'),
|
|
833
|
+
null,
|
|
834
|
+
{ timeout: 15_000 },
|
|
835
|
+
)
|
|
836
|
+
ok('an undecodable ledger is reported as such')
|
|
837
|
+
|
|
838
|
+
ledger.mode = 'ok'
|
|
839
|
+
|
|
840
|
+
// 8. Sending coin. A different transaction type: validation layer rather than
|
|
841
|
+
// application layer, so one blob to submit rather than two, posted as `uvt`.
|
|
842
|
+
ledger.mode = 'ok'
|
|
843
|
+
ledger.block = 43
|
|
844
|
+
ledger.nonce = 9
|
|
845
|
+
await chainCard.locator('[data-testid=connect]').click()
|
|
846
|
+
await chainCard.locator('[data-testid=selected-node]').waitFor({ timeout: 20_000 })
|
|
847
|
+
|
|
848
|
+
// A third-party recipient. Built with the SDK itself rather than scraped, because the
|
|
849
|
+
// page's own address would be a transfer to self, which the SDK refuses outright.
|
|
850
|
+
await geeqSdk.initGeeq()
|
|
851
|
+
const recipientId = new Uint8Array(32).fill(0x5a)
|
|
852
|
+
const receiverAddress = await geeqSdk.createWalletAddress({
|
|
853
|
+
accountId: recipientId,
|
|
854
|
+
chainNumber: 1,
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
// Amounts name their unit, and the page shows the pennies a typed figure comes to.
|
|
858
|
+
await transferCard.locator('[data-testid=amount]').fill('1.50')
|
|
859
|
+
assert.equal(
|
|
860
|
+
(await transferCard.locator('[data-testid=pennies]').textContent()).trim(),
|
|
861
|
+
String(geeq('1.50')),
|
|
862
|
+
'the typed figure is shown in pennies',
|
|
863
|
+
)
|
|
864
|
+
ok("an amount is read as pennies, at the app's pennies-per-geeq")
|
|
865
|
+
|
|
866
|
+
// One decimal place more than the unit has is money the chain cannot represent, so it is
|
|
867
|
+
// refused outright.
|
|
868
|
+
await transferCard.locator('[data-testid=amount]').fill(`0.${'1'.padStart(String(penniesPerGeeq).length, '0')}`)
|
|
869
|
+
await transferCard.locator('[data-testid=amount-problem]').waitFor({ timeout: 5_000 })
|
|
870
|
+
assert.match(
|
|
871
|
+
await transferCard.locator('[data-testid=amount-problem]').textContent(),
|
|
872
|
+
/are possible/,
|
|
873
|
+
'a decimal place past the unit is refused rather than rounded',
|
|
874
|
+
)
|
|
875
|
+
ok('an impossible amount is refused, not rounded')
|
|
876
|
+
|
|
877
|
+
// Below the chain's minimum: 0.1 geeq is minTxAmt x geeqNorm, one penny short of it.
|
|
878
|
+
await transferCard.locator('[data-testid=amount]').fill('0.1')
|
|
879
|
+
await transferCard.locator('[data-testid=recipient]').fill(receiverAddress)
|
|
880
|
+
await transferCard.locator('[data-testid=build-transfer]').click()
|
|
881
|
+
await transferCard.locator('[data-testid=transfer-error]').waitFor({ timeout: 20_000 })
|
|
882
|
+
assert.match(
|
|
883
|
+
await transferCard.locator('[data-testid=transfer-error]').textContent(),
|
|
884
|
+
new RegExp(`below ${shownPattern(MINIMUM)} geeq`),
|
|
885
|
+
'the chain minimum is read from the ledger and explained',
|
|
886
|
+
)
|
|
887
|
+
ok('an amount below the chain minimum is refused')
|
|
888
|
+
|
|
889
|
+
// More than the account holds.
|
|
890
|
+
await transferCard.locator('[data-testid=amount]').fill('9999')
|
|
891
|
+
await transferCard.locator('[data-testid=build-transfer]').click()
|
|
892
|
+
await page.waitForFunction(
|
|
893
|
+
() => document.querySelector('[data-testid=transfer-error]')?.textContent?.includes('holds'),
|
|
894
|
+
null,
|
|
895
|
+
{ timeout: 15_000 },
|
|
896
|
+
)
|
|
897
|
+
ok('a transfer larger than the balance is refused')
|
|
898
|
+
|
|
899
|
+
// And one that works.
|
|
900
|
+
await transferCard.locator('[data-testid=amount]').fill('1.50')
|
|
901
|
+
await transferCard.locator('[data-testid=build-transfer]').click()
|
|
902
|
+
await transferCard.locator('[data-testid=transfer-base64]').waitFor({ timeout: 20_000 })
|
|
903
|
+
|
|
904
|
+
assert.match(
|
|
905
|
+
await transferCard.locator('[data-testid=balance]').textContent(),
|
|
906
|
+
new RegExp(`1000 geeq \\(${geeq('1000')} pennies\\)`),
|
|
907
|
+
'the balance is read from the ledger and shown, in geeq and pennies',
|
|
908
|
+
)
|
|
909
|
+
assert.match(
|
|
910
|
+
await transferCard.locator('[data-testid=transfer-summary]').textContent(),
|
|
911
|
+
new RegExp(`^1\\.5 geeq \\(${geeq('1.50')} pennies\\) →`),
|
|
912
|
+
'the amount is shown back in geeq, with its pennies beside it',
|
|
913
|
+
)
|
|
914
|
+
ok('a coin transfer is built and signed')
|
|
915
|
+
|
|
916
|
+
const before = submitted.length
|
|
917
|
+
await transferCard.locator('[data-testid=submit-transfer]').click()
|
|
918
|
+
await transferCard.locator('[data-testid=tx-status][data-status=pending]').waitFor({ timeout: 15_000 })
|
|
919
|
+
assert.equal(submitted.length, before + 1, 'the transfer reached the node')
|
|
920
|
+
|
|
921
|
+
const postedTransfer = submitted[submitted.length - 1]
|
|
922
|
+
assert.equal(postedTransfer.type, 'uvt', 'a transfer posts as uvt, not uat')
|
|
923
|
+
assert.equal(postedTransfer.data.payload, undefined, 'a transfer has no separate payload')
|
|
924
|
+
assert.equal(
|
|
925
|
+
postedTransfer.data.transaction,
|
|
926
|
+
(await transferCard.locator('[data-testid=transfer-base64]').textContent()).trim(),
|
|
927
|
+
'posted exactly what was signed',
|
|
928
|
+
)
|
|
929
|
+
ok('the transfer posts one blob as uvt, unlike evidence')
|
|
930
|
+
|
|
931
|
+
// The fee the page shows is what the node charges for the bytes it actually received.
|
|
932
|
+
const transferFee = nodeFee(postedTransfer, geeq('1.50'))
|
|
933
|
+
assert.equal(
|
|
934
|
+
(await transferCard.locator('[data-testid=tx-fee-total]').textContent()).trim(),
|
|
935
|
+
shown(transferFee),
|
|
936
|
+
'the fee shown is the node formula over the posted bytes',
|
|
937
|
+
)
|
|
938
|
+
assert.equal(
|
|
939
|
+
(await transferCard.locator('[data-testid=tx-fee-pennies]').textContent()).trim(),
|
|
940
|
+
`(${transferFee} pennies)`,
|
|
941
|
+
'and the same fee in pennies beside it',
|
|
942
|
+
)
|
|
943
|
+
assert.equal(
|
|
944
|
+
(await transferCard.locator('[data-testid=tx-cost]').textContent()).trim(),
|
|
945
|
+
shown(geeq('1.50') + transferFee),
|
|
946
|
+
'and the account pays the amount plus the fee',
|
|
947
|
+
)
|
|
948
|
+
ok('the transfer shows the fee the node will charge, and the total it costs')
|
|
949
|
+
|
|
950
|
+
// Close the loop: the values the fixture published are the ones in the signed bytes.
|
|
951
|
+
const transferData = new Message(
|
|
952
|
+
Buffer.from(postedTransfer.data.transaction, 'base64'),
|
|
953
|
+
true,
|
|
954
|
+
).getRoot(UserValTx).getData()
|
|
955
|
+
assert.equal(transferData.getChainNum(), 1, 'signed chain number')
|
|
956
|
+
assert.equal(transferData.getBlockNum(), 44, 'signed target block')
|
|
957
|
+
assert.equal(transferData.getNodeNum(), 3, 'signed node number')
|
|
958
|
+
const assetTx = transferData.getTx().getCoin().getTxType().getAsset()
|
|
959
|
+
assert.equal(assetTx.getNonce(), 9, 'signed nonce')
|
|
960
|
+
assert.equal(assetTx.getAmtCoin().getSome().toDataView().getBigUint64(0, true), geeq('1.50'), 'signed amount')
|
|
961
|
+
assert.deepEqual(
|
|
962
|
+
Buffer.from(assetTx.getAcctNumRcv().toArrayBuffer()),
|
|
963
|
+
Buffer.from(recipientId),
|
|
964
|
+
'the pasted address resolved to the account id it encodes',
|
|
965
|
+
)
|
|
966
|
+
ok('the transfer carries the amount, recipient and chain state that were derived')
|
|
967
|
+
|
|
968
|
+
// A transfer is filed under the hash of its UNSIGNED flatten — the opposite of
|
|
969
|
+
// evidence. Hashing the signed form would give an id no status event ever carries.
|
|
970
|
+
const transferId = nodeRefHash(postedTransfer)
|
|
971
|
+
assert.equal(
|
|
972
|
+
(await transferCard.locator('[data-testid=tx-id]').textContent()).trim(),
|
|
973
|
+
transferId,
|
|
974
|
+
'the SDK hashes a transfer the way the node does — signature excluded',
|
|
975
|
+
)
|
|
976
|
+
ok('the transfer id matches the node refHash for the posted bytes')
|
|
977
|
+
|
|
978
|
+
// A nonsane is not necessarily the last word; a later verdict decides.
|
|
979
|
+
uutStatus(transferId, 'nonsane', { error: 'Choose list sets operation: nonce' })
|
|
980
|
+
await transferCard.locator('[data-testid=tx-events] li').first().waitFor({ timeout: 5_000 })
|
|
981
|
+
assert.equal(
|
|
982
|
+
await transferCard.locator('[data-testid=tx-status]').getAttribute('data-status'),
|
|
983
|
+
'pending',
|
|
984
|
+
'a nonsane alone does not settle it',
|
|
985
|
+
)
|
|
986
|
+
uutStatus(transferId, 'invalid', { error: 'Nonce Check failed' })
|
|
987
|
+
await transferCard
|
|
988
|
+
.locator('[data-testid=tx-status][data-status=rejected][data-reason=invalid]')
|
|
989
|
+
.waitFor({ timeout: 5_000 })
|
|
990
|
+
const transferStatus = await transferCard.locator('[data-testid=tx-status]').textContent()
|
|
991
|
+
assert.match(transferStatus, /fee is still charged/, 'an invalid transfer says the fee is still charged')
|
|
992
|
+
assert.match(transferStatus, /Nonce Check failed/, 'with the node reason')
|
|
993
|
+
assert.equal(await transferCard.locator('[data-testid=tx-events] li').count(), 2, 'both events kept')
|
|
994
|
+
assert.match(
|
|
995
|
+
await transferCard.locator('[data-testid=tx-events] li').first().textContent(),
|
|
996
|
+
/nonsane · block 42 · outgoing/,
|
|
997
|
+
'each event shows its verdict, block and direction',
|
|
998
|
+
)
|
|
999
|
+
ok('nonsane then invalid settles the transfer rejected, with both events kept')
|
|
1000
|
+
|
|
1001
|
+
// Anything else the node pushes reaches the page through the same class.
|
|
1002
|
+
push('coin-account-update', { buffer: 'AA==', block: 44 })
|
|
1003
|
+
await page.waitForFunction(
|
|
1004
|
+
() => document.querySelector('[data-testid=socket-events]')?.textContent?.includes('coin-account-update'),
|
|
1005
|
+
null,
|
|
1006
|
+
{ timeout: 5_000 },
|
|
1007
|
+
)
|
|
1008
|
+
ok('an arbitrary node event is delivered to the page')
|
|
1009
|
+
|
|
1010
|
+
// A node forgets a connection's subscriptions when it drops, so the page must tell the new
|
|
1011
|
+
// one everything again — on its own, before it calls the socket open.
|
|
1012
|
+
const dialled = sockets.length
|
|
1013
|
+
await nodeSocket().route.close()
|
|
1014
|
+
await chainCard.locator('[data-testid=socket][data-state=reconnecting]').waitFor({ timeout: 5_000 })
|
|
1015
|
+
await chainCard.locator('[data-testid=socket][data-state=open]').waitFor({ timeout: 10_000 })
|
|
1016
|
+
assert.equal(sockets.length, dialled + 1, 'the page dialled once more, by itself')
|
|
1017
|
+
await subscribedTo(accountIdB64)
|
|
1018
|
+
ok('a dropped socket reconnects and replays its subscriptions')
|
|
1019
|
+
|
|
1020
|
+
// A socket that dropped and cannot come back cannot take the subscription. The SDK gives
|
|
1021
|
+
// up after its 15 s open timeout, and the page sends the transaction anyway, saying so.
|
|
1022
|
+
socketDown = true
|
|
1023
|
+
ledger.block = 44
|
|
1024
|
+
ledger.nonce = 10
|
|
1025
|
+
await nodeSocket().route.close()
|
|
1026
|
+
await chainCard.locator('[data-testid=socket][data-state=reconnecting]').waitFor({ timeout: 5_000 })
|
|
1027
|
+
await pasteHash('no verdict')
|
|
1028
|
+
await evidenceCard.getByRole('button', { name: 'Build & sign' }).click()
|
|
1029
|
+
await evidenceCard.locator('[data-testid=tx-base64]').waitFor({ timeout: 20_000 })
|
|
1030
|
+
let sent = submitted.length
|
|
1031
|
+
await evidenceCard.locator('[data-testid=submit]').click()
|
|
1032
|
+
await evidenceCard.locator('[data-testid=tx-untracked]').waitFor({ timeout: 25_000 })
|
|
1033
|
+
assert.equal(submitted.length, sent + 1, 'sent anyway')
|
|
1034
|
+
assert.match(
|
|
1035
|
+
await evidenceCard.locator('[data-testid=tx-untracked]').textContent(),
|
|
1036
|
+
/no confirmation will follow[\s\S]*Could not subscribe/,
|
|
1037
|
+
'and says why no verdict will be shown',
|
|
1038
|
+
)
|
|
1039
|
+
ok('a subscription that fails still sends the transaction, with a warning')
|
|
1040
|
+
|
|
1041
|
+
// No socket at all — the first handshake fails, as an untrusted certificate makes it. The
|
|
1042
|
+
// page warns, submits anyway, and recovers when asked to retry.
|
|
1043
|
+
ledger.block = 45
|
|
1044
|
+
ledger.nonce = 11
|
|
1045
|
+
await chainCard.locator('[data-testid=connect]').click()
|
|
1046
|
+
await chainCard.locator('[data-testid=socket][data-state=failed]').waitFor({ timeout: 20_000 })
|
|
1047
|
+
assert.match(
|
|
1048
|
+
await chainCard.locator('[data-testid=socket-error]').textContent(),
|
|
1049
|
+
/Could not open the Geeq node socket/,
|
|
1050
|
+
'step 3 says why',
|
|
1051
|
+
)
|
|
1052
|
+
await pasteHash('no socket')
|
|
1053
|
+
await evidenceCard.getByRole('button', { name: 'Build & sign' }).click()
|
|
1054
|
+
await evidenceCard.locator('[data-testid=tx-base64]').waitFor({ timeout: 20_000 })
|
|
1055
|
+
const warning = evidenceCard.locator('[data-testid=socket-warning]')
|
|
1056
|
+
assert.match(
|
|
1057
|
+
await warning.textContent(),
|
|
1058
|
+
/No confirmation will follow[\s\S]*Could not open the Geeq node socket/,
|
|
1059
|
+
'the panel warns before anything is sent, with the socket error',
|
|
1060
|
+
)
|
|
1061
|
+
sent = submitted.length
|
|
1062
|
+
await evidenceCard.locator('[data-testid=submit]').click()
|
|
1063
|
+
await evidenceCard.locator('[data-testid=tx-untracked]').waitFor({ timeout: 15_000 })
|
|
1064
|
+
assert.equal(submitted.length, sent + 1, 'submitted anyway')
|
|
1065
|
+
assert.equal(await evidenceCard.locator('[data-testid=tx-status]').count(), 0, 'no earlier verdict left standing under it')
|
|
1066
|
+
socketDown = false
|
|
1067
|
+
await warning.locator('[data-testid=socket-retry]').click()
|
|
1068
|
+
await chainCard.locator('[data-testid=socket][data-state=open]').waitFor({ timeout: 10_000 })
|
|
1069
|
+
assert.equal(await warning.count(), 0, 'the warning clears once the socket is open')
|
|
1070
|
+
await subscribedTo(accountIdB64)
|
|
1071
|
+
ok('with no socket the page warns, still submits, and Retry recovers')
|
|
1072
|
+
|
|
1073
|
+
// 10. Delegating authority. A validation-layer transaction like a transfer, naming this
|
|
1074
|
+
// account as the authority account's admin, with an id derived from its key.
|
|
1075
|
+
const authorityCard = page.locator('section.card').nth(5)
|
|
1076
|
+
ledger.block = 46
|
|
1077
|
+
ledger.nonce = 12
|
|
1078
|
+
|
|
1079
|
+
// The chain's minimum applies to the funding too.
|
|
1080
|
+
await authorityCard.locator('[data-testid=authority-amount]').fill('0.1')
|
|
1081
|
+
await authorityCard.locator('[data-testid=build-authority]').click()
|
|
1082
|
+
await authorityCard.locator('[data-testid=authority-error]').waitFor({ timeout: 20_000 })
|
|
1083
|
+
assert.match(
|
|
1084
|
+
await authorityCard.locator('[data-testid=authority-error]').textContent(),
|
|
1085
|
+
new RegExp(`at least ${shownPattern(MINIMUM)} geeq`),
|
|
1086
|
+
'names the minimum',
|
|
1087
|
+
)
|
|
1088
|
+
ok('authority account funding below the chain minimum is refused')
|
|
1089
|
+
|
|
1090
|
+
await authorityCard.locator('[data-testid=authority-amount]').fill('20')
|
|
1091
|
+
await authorityCard.locator('[data-testid=build-authority]').click()
|
|
1092
|
+
await authorityCard.locator('[data-testid=authority-base64]').waitFor({ timeout: 20_000 })
|
|
1093
|
+
// The id the Geeq apps derive for this key's authority account at an index, computed
|
|
1094
|
+
// independently.
|
|
1095
|
+
const derivedAuthorityId = (index) =>
|
|
1096
|
+
Buffer.from(
|
|
1097
|
+
wasm.generateIrn(wasm.IrnRecordType.ProxyNftMint, Buffer.from(publicKeyB64, 'base64'), index),
|
|
1098
|
+
).toString('base64')
|
|
1099
|
+
const authorityId = derivedAuthorityId(1)
|
|
1100
|
+
assert.equal((await authorityCard.locator('[data-testid=authority-index]').textContent()).trim(), '1')
|
|
1101
|
+
assert.equal((await authorityCard.locator('[data-testid=authority-id]').textContent()).trim(), authorityId)
|
|
1102
|
+
ok("an account's first authority account takes index 1, with the id the Geeq apps would derive")
|
|
1103
|
+
|
|
1104
|
+
sent = submitted.length
|
|
1105
|
+
await authorityCard.locator('[data-testid=submit-authority]').click()
|
|
1106
|
+
await authorityCard.locator('[data-testid=tx-status][data-status=pending]').waitFor({ timeout: 15_000 })
|
|
1107
|
+
assert.equal(submitted.length, sent + 1, 'the delegation reached the node')
|
|
1108
|
+
assert.equal(
|
|
1109
|
+
(await authorityCard.locator('[data-testid=admin-nonce]').textContent()).trim(),
|
|
1110
|
+
'12',
|
|
1111
|
+
"the page says which of the account's nonces it spends",
|
|
1112
|
+
)
|
|
1113
|
+
assert.ok(
|
|
1114
|
+
await authorityCard.locator('[data-testid=submit-authority]').isDisabled(),
|
|
1115
|
+
'a submitted delegation cannot be sent a second time',
|
|
1116
|
+
)
|
|
1117
|
+
const postedDelegation = submitted[submitted.length - 1]
|
|
1118
|
+
assert.equal(postedDelegation.type, 'uvt', 'a delegation posts as uvt, like a transfer')
|
|
1119
|
+
const delegationData = new Message(Buffer.from(postedDelegation.data.transaction, 'base64'), true)
|
|
1120
|
+
.getRoot(UserValTx)
|
|
1121
|
+
.getData()
|
|
1122
|
+
assert.equal(delegationData.getBlockNum(), 47, 'signed target block')
|
|
1123
|
+
const delegation = delegationData.getTx().getProxyCreate()
|
|
1124
|
+
assert.equal(delegation.getNonce(), 12, "spends the admin's nonce")
|
|
1125
|
+
assert.equal(delegation.getAmtCoin().toDataView().getBigUint64(0, true), geeq('20'), 'funds 20 geeq')
|
|
1126
|
+
assert.equal(Buffer.from(delegation.getAcctNumRcv().toUint8Array()).toString('base64'), authorityId)
|
|
1127
|
+
assert.equal(
|
|
1128
|
+
Buffer.from(delegation.getTxType().getNftMint().getAcctNumAdminUser().toUint8Array()).toString('base64'),
|
|
1129
|
+
accountIdB64,
|
|
1130
|
+
'this account is the admin',
|
|
1131
|
+
)
|
|
1132
|
+
ok('the delegation funds the derived id, with this account as admin')
|
|
1133
|
+
|
|
1134
|
+
const delegationId = nodeRefHash(postedDelegation)
|
|
1135
|
+
assert.equal((await authorityCard.locator('[data-testid=tx-id]').textContent()).trim(), delegationId)
|
|
1136
|
+
uutStatus(delegationId, 'valid')
|
|
1137
|
+
await authorityCard.locator('[data-testid=tx-status][data-status=confirmed]').waitFor({ timeout: 5_000 })
|
|
1138
|
+
ok('the delegation is confirmed over the socket')
|
|
1139
|
+
|
|
1140
|
+
// The ledger lists an authority account only once a block has confirmed it, and finds it from the
|
|
1141
|
+
// admin's key alone.
|
|
1142
|
+
await authorityCard.locator('[data-testid=find-authorities]').click()
|
|
1143
|
+
await authorityCard.locator('[data-testid=no-authorities]').waitFor({ timeout: 10_000 })
|
|
1144
|
+
assert.equal((await authorityCard.locator('[data-testid=next-index]').textContent()).trim(), '1')
|
|
1145
|
+
// The block that adds the authority account also spends the account's nonce.
|
|
1146
|
+
ledger.block = 47
|
|
1147
|
+
ledger.authorityId = authorityId
|
|
1148
|
+
ledger.nonce = 13
|
|
1149
|
+
await authorityCard.locator('[data-testid=find-authorities]').click()
|
|
1150
|
+
await authorityCard.locator('[data-testid=authority]').waitFor({ timeout: 10_000 })
|
|
1151
|
+
assert.equal(
|
|
1152
|
+
(await authorityCard.locator('[data-testid=authority] .v').textContent()).trim(),
|
|
1153
|
+
(await authorityCard.locator('[data-testid=authority-address]').textContent()).trim(),
|
|
1154
|
+
'the one it built',
|
|
1155
|
+
)
|
|
1156
|
+
assert.match(
|
|
1157
|
+
await authorityCard.locator('[data-testid=authority]').textContent(),
|
|
1158
|
+
new RegExp(`Authority account 1[\\s\\S]*20 geeq \\(${geeq('20')} pennies\\) · own nonce 1`),
|
|
1159
|
+
'with its index, balance and own nonce',
|
|
1160
|
+
)
|
|
1161
|
+
assert.equal((await authorityCard.locator('[data-testid=next-index]').textContent()).trim(), '2')
|
|
1162
|
+
ok('the confirmed authority account is found in the ledger by its admin, and the next index is 2')
|
|
1163
|
+
|
|
1164
|
+
// The next authority account takes the next index, found from the ledger rather than
|
|
1165
|
+
// remembered.
|
|
1166
|
+
await authorityCard.locator('[data-testid=build-authority]').click()
|
|
1167
|
+
await authorityCard.locator('[data-testid=authority-index]', { hasText: '2' }).waitFor({ timeout: 20_000 })
|
|
1168
|
+
assert.equal((await authorityCard.locator('[data-testid=authority-id]').textContent()).trim(), derivedAuthorityId(2))
|
|
1169
|
+
assert.equal(
|
|
1170
|
+
(await authorityCard.locator('[data-testid=admin-nonce]').textContent()).trim(),
|
|
1171
|
+
'13',
|
|
1172
|
+
"the account's next nonce, not the authority account's",
|
|
1173
|
+
)
|
|
1174
|
+
assert.ok(await authorityCard.locator('[data-testid=submit-authority]').isEnabled(), 'a new build can be sent')
|
|
1175
|
+
ok("a second authority account takes index 2 and the account's next nonce")
|
|
1176
|
+
|
|
1177
|
+
// 11. Creating a digital asset through the confirmed authority account. An application-layer
|
|
1178
|
+
// transaction like evidence, sent by the authority account on its own nonce and signed by
|
|
1179
|
+
// this account as its admin.
|
|
1180
|
+
const createAssetCard = page.locator('section.card').nth(6)
|
|
1181
|
+
assert.ok(await createAssetCard.locator('[data-testid=build-asset]').isDisabled(), 'nothing to create through yet')
|
|
1182
|
+
await createAssetCard.locator('[data-testid=find-asset-authorities]').click()
|
|
1183
|
+
await createAssetCard.locator('[data-testid=asset-authority] option', { hasText: 'Authority account 1' }).waitFor({ state: 'attached', timeout: 10_000 })
|
|
1184
|
+
assert.equal(await createAssetCard.locator('[data-testid=asset-authority]').inputValue(), '1', 'the first authority account is chosen')
|
|
1185
|
+
assert.match(
|
|
1186
|
+
await createAssetCard.locator('[data-testid=asset-authority]').textContent(),
|
|
1187
|
+
new RegExp(`Authority account 1 · 20 geeq\\s+\\(${geeq('20')} pennies\\)`),
|
|
1188
|
+
'listed with its balance',
|
|
1189
|
+
)
|
|
1190
|
+
ok("the account's authority accounts are offered to create through")
|
|
1191
|
+
|
|
1192
|
+
const deed = Buffer.from('a deed')
|
|
1193
|
+
await createAssetCard.locator('[data-testid=asset-file]').setInputFiles({ name: 'deed.txt', mimeType: 'text/plain', buffer: deed })
|
|
1194
|
+
await createAssetCard.locator('[data-testid=asset-metadata]').fill('deed')
|
|
1195
|
+
await createAssetCard.locator('[data-testid=build-asset]').click()
|
|
1196
|
+
await createAssetCard.locator('[data-testid=asset-base64]').waitFor({ timeout: 20_000 })
|
|
1197
|
+
// Changing a label drops that build unsent, so building again in the same block goes through.
|
|
1198
|
+
await createAssetCard.locator('[data-testid=asset-metadata]').fill('deed, lot 7')
|
|
1199
|
+
await createAssetCard.locator('[data-testid=asset-base64]').waitFor({ state: 'detached', timeout: 5_000 })
|
|
1200
|
+
await createAssetCard.locator('[data-testid=build-asset]').click()
|
|
1201
|
+
await createAssetCard.locator('[data-testid=asset-base64]').waitFor({ timeout: 20_000 })
|
|
1202
|
+
assert.equal(await createAssetCard.locator('[data-testid=asset-error]').count(), 0, 'the rebuild is not refused')
|
|
1203
|
+
assert.equal(
|
|
1204
|
+
(await createAssetCard.locator('[data-testid=authority-nonce]').textContent()).trim(),
|
|
1205
|
+
'1',
|
|
1206
|
+
"the authority account's own nonce, not the account's 13",
|
|
1207
|
+
)
|
|
1208
|
+
assert.match(
|
|
1209
|
+
await createAssetCard.locator('[data-testid=chain-state] tr', { hasText: 'Nonce' }).textContent(),
|
|
1210
|
+
/Nonce\s*1\s*the number authority account 1's next transaction must carry/,
|
|
1211
|
+
'the chain state table shows the same, as the authority account\'s',
|
|
1212
|
+
)
|
|
1213
|
+
ok('a build that was never sent is rebuilt in the same block, on the same nonce')
|
|
1214
|
+
|
|
1215
|
+
sent = submitted.length
|
|
1216
|
+
await createAssetCard.locator('[data-testid=submit-asset]').click()
|
|
1217
|
+
await createAssetCard.locator('[data-testid=tx-status][data-status=pending]').waitFor({ timeout: 15_000 })
|
|
1218
|
+
assert.equal(submitted.length, sent + 1, 'the asset creation reached the node')
|
|
1219
|
+
const postedAsset = submitted[submitted.length - 1]
|
|
1220
|
+
assert.equal(postedAsset.type, 'uat', 'an asset creation posts as uat, like evidence')
|
|
1221
|
+
const assetData = new Message(Buffer.from(postedAsset.data.transaction, 'base64'), true)
|
|
1222
|
+
.getRoot(UserAppTx)
|
|
1223
|
+
.getData()
|
|
1224
|
+
assert.equal(assetData.getBlockNum(), 48, 'signed target block')
|
|
1225
|
+
assert.equal(assetData.getNonce(), 1, "spends the authority account's nonce")
|
|
1226
|
+
assert.equal(Buffer.from(assetData.getPubKey().toUint8Array()).toString('base64'), publicKeyB64, 'signed by the admin')
|
|
1227
|
+
const creation = assetData.getTxType().getNftMint()
|
|
1228
|
+
assert.equal(Buffer.from(creation.getAcctNumVal().toUint8Array()).toString('base64'), authorityId, 'sent by the authority account')
|
|
1229
|
+
assert.equal(Buffer.from(creation.getAcctNumRcv().toUint8Array()).toString('base64'), accountIdB64, 'to this account')
|
|
1230
|
+
assert.equal(creation.getAmtCoin().toDataView().getBigUint64(0, true), MINIMUM, "with the chain's minimum")
|
|
1231
|
+
const assetId = Buffer.from(creation.getAcctNumApp().toUint8Array()).toString('base64')
|
|
1232
|
+
assert.equal((await createAssetCard.locator('[data-testid=asset-id]').textContent()).trim(), assetId)
|
|
1233
|
+
const assetPayload = new Message(Buffer.from(postedAsset.data.payload, 'base64'), true)
|
|
1234
|
+
.getRoot(Payload)
|
|
1235
|
+
.getTxType()
|
|
1236
|
+
.getNftMint()
|
|
1237
|
+
const payloadData = assetPayload.getAttestationSubgroup().getData()
|
|
1238
|
+
assert.equal(Buffer.from(assetPayload.getAcctNumApp().toUint8Array()).toString('base64'), assetId, 'payload and transaction agree')
|
|
1239
|
+
assert.deepEqual(Buffer.from(payloadData.getAttestHash().toUint8Array()), Buffer.from(blake3(deed, { dkLen: 32 })), "the file's hash")
|
|
1240
|
+
assert.deepEqual(
|
|
1241
|
+
payloadData.getMetadata().toArray().map((entry) => Buffer.from(entry.getItem().toUint8Array()).toString()),
|
|
1242
|
+
['deed', 'lot 7'],
|
|
1243
|
+
)
|
|
1244
|
+
assert.equal(Buffer.from(payloadData.getAcctNumVal().toUint8Array()).toString('base64'), authorityId, 'naming the authority account')
|
|
1245
|
+
ok("the asset creation is sent by the authority account on its nonce, to this account, carrying the file's hash")
|
|
1246
|
+
|
|
1247
|
+
// Creating a digital asset pays for its payload's bytes too, and the authority account pays it.
|
|
1248
|
+
assert.equal(
|
|
1249
|
+
(await createAssetCard.locator('[data-testid=tx-fee-total]').textContent()).trim(),
|
|
1250
|
+
shown(nodeFee(postedAsset, MINIMUM)),
|
|
1251
|
+
'the fee shown is the node formula over the posted transaction and payload',
|
|
1252
|
+
)
|
|
1253
|
+
assert.match(await createAssetCard.locator('[data-testid=tx-fee]').textContent(), /paid by authority account 1/)
|
|
1254
|
+
ok('the asset creation shows the fee the authority account will pay')
|
|
1255
|
+
|
|
1256
|
+
const assetTxId = nodeRefHash(postedAsset)
|
|
1257
|
+
assert.equal((await createAssetCard.locator('[data-testid=tx-id]').textContent()).trim(), assetTxId)
|
|
1258
|
+
uutStatus(assetTxId, 'valid')
|
|
1259
|
+
await createAssetCard.locator('[data-testid=tx-status][data-status=confirmed]').waitFor({ timeout: 5_000 })
|
|
1260
|
+
ok('the asset creation is confirmed over the socket')
|
|
1261
|
+
|
|
1262
|
+
// One that was sent keeps its nonce: a second creation through the authority account waits
|
|
1263
|
+
// for the block.
|
|
1264
|
+
await createAssetCard.locator('[data-testid=build-asset]').click()
|
|
1265
|
+
await createAssetCard.locator('[data-testid=asset-error]').waitFor({ timeout: 20_000 })
|
|
1266
|
+
assert.match(
|
|
1267
|
+
await createAssetCard.locator('[data-testid=asset-error]').textContent(),
|
|
1268
|
+
/already has a transaction built with nonce 1 against block 47/,
|
|
1269
|
+
)
|
|
1270
|
+
ok('an asset creation that was sent is not rebuilt on its nonce in the same block')
|
|
1271
|
+
|
|
1272
|
+
// 12. The account's digital assets. The block that confirms the creation puts the digital
|
|
1273
|
+
// asset on this account's record; what it stands for is in the application layer ledger.
|
|
1274
|
+
const assetsCard = page.locator('section.card').nth(7)
|
|
1275
|
+
await assetsCard.locator('[data-testid=get-assets]').click()
|
|
1276
|
+
await assetsCard.locator('[data-testid=no-assets]').waitFor({ timeout: 10_000 })
|
|
1277
|
+
ledger.block = 48
|
|
1278
|
+
const ticketId = Buffer.alloc(32, 0x77).toString('base64')
|
|
1279
|
+
ledger.assets = [
|
|
1280
|
+
{ id: assetId, attestHash: blake3(deed, { dkLen: 32 }), metadata: ['deed', 'lot 7'] },
|
|
1281
|
+
{ id: ticketId, attestHash: Buffer.alloc(32, 0x11), metadata: ['ticket'] },
|
|
1282
|
+
]
|
|
1283
|
+
await assetsCard.locator('[data-testid=get-assets]').click()
|
|
1284
|
+
await assetsCard.locator('[data-testid=asset]').nth(1).waitFor({ timeout: 10_000 })
|
|
1285
|
+
const deedAsset = await assetsCard.locator('[data-testid=asset]').first().textContent()
|
|
1286
|
+
assert.match(deedAsset, /deed, lot 7/, 'with its labels, in order')
|
|
1287
|
+
assert.ok(deedAsset.includes(assetId), 'and its id')
|
|
1288
|
+
assert.ok(deedAsset.includes(Buffer.from(blake3(deed, { dkLen: 32 })).toString('hex')), "and the file's hash")
|
|
1289
|
+
ok("the account's digital assets are listed from the ledger, with what each was created with")
|
|
1290
|
+
|
|
1291
|
+
// 13. Sending digital assets: a transfer that names them, with the chain's minimum as its coin.
|
|
1292
|
+
await transferCard.locator('[data-testid=send-assets]').check()
|
|
1293
|
+
assert.equal(await transferCard.locator('[data-testid=amount]').count(), 0, 'no amount to type')
|
|
1294
|
+
await transferCard.locator('[data-testid=transfer-get-assets]').click()
|
|
1295
|
+
await transferCard.locator('[data-testid=asset-choice]').nth(1).waitFor({ timeout: 10_000 })
|
|
1296
|
+
await transferCard.locator('[data-testid=asset-choice]').first().check()
|
|
1297
|
+
await transferCard.locator('[data-testid=build-transfer]').click()
|
|
1298
|
+
await transferCard.locator('[data-testid=transfer-base64]').waitFor({ timeout: 20_000 })
|
|
1299
|
+
assert.match(await transferCard.locator('[data-testid=transfer-summary]').textContent(), new RegExp(`^\\s*1 digital asset \\+ ${shownPattern(MINIMUM)} geeq \\(${MINIMUM} pennies\\) →`))
|
|
1300
|
+
// Ticking another drops that build unsent, so building again in the same block goes through.
|
|
1301
|
+
await transferCard.locator('[data-testid=asset-choice]').nth(1).check()
|
|
1302
|
+
await transferCard.locator('[data-testid=transfer-base64]').waitFor({ state: 'detached', timeout: 5_000 })
|
|
1303
|
+
await transferCard.locator('[data-testid=build-transfer]').click()
|
|
1304
|
+
await transferCard.locator('[data-testid=transfer-summary]', { hasText: `2 digital assets + ${shown(MINIMUM)} geeq` }).waitFor({ timeout: 20_000 })
|
|
1305
|
+
assert.equal(await transferCard.locator('[data-testid=transfer-error]').count(), 0, 'the rebuild is not refused')
|
|
1306
|
+
ok('one digital asset or several are built into a transfer, rebuilt freely until sent')
|
|
1307
|
+
|
|
1308
|
+
sent = submitted.length
|
|
1309
|
+
await transferCard.locator('[data-testid=submit-transfer]').click()
|
|
1310
|
+
await transferCard.locator('[data-testid=tx-status][data-status=pending]').waitFor({ timeout: 15_000 })
|
|
1311
|
+
assert.equal(submitted.length, sent + 1, 'the digital asset transfer reached the node')
|
|
1312
|
+
const postedAssets = submitted[submitted.length - 1]
|
|
1313
|
+
assert.equal(postedAssets.type, 'uvt', 'posted as a transfer')
|
|
1314
|
+
const assetsTransfer = new Message(Buffer.from(postedAssets.data.transaction, 'base64'), true)
|
|
1315
|
+
.getRoot(UserValTx)
|
|
1316
|
+
.getData()
|
|
1317
|
+
assert.equal(assetsTransfer.getBlockNum(), 49, 'signed target block')
|
|
1318
|
+
const transferTx = assetsTransfer.getTx().getCoin().getTxType().getAsset()
|
|
1319
|
+
assert.equal(transferTx.getNonce(), 13, "spends the account's nonce")
|
|
1320
|
+
assert.equal(transferTx.getAmtCoin().getSome().toDataView().getBigUint64(0, true), MINIMUM, "with the chain's minimum")
|
|
1321
|
+
assert.deepEqual(Buffer.from(transferTx.getAcctNumRcv().toArrayBuffer()), Buffer.from(recipientId), 'to the pasted address')
|
|
1322
|
+
assert.deepEqual(
|
|
1323
|
+
transferTx.getNftAssetSubgroups().toArray().map((entry) => [
|
|
1324
|
+
entry.getPosition(),
|
|
1325
|
+
Buffer.from(entry.getItem().getAcctNumApp().toUint8Array()).toString('base64'),
|
|
1326
|
+
]),
|
|
1327
|
+
[[0, assetId], [1, ticketId]],
|
|
1328
|
+
"each digital asset with its place in the account's record",
|
|
1329
|
+
)
|
|
1330
|
+
const assetsTransferId = nodeRefHash(postedAssets)
|
|
1331
|
+
assert.equal((await transferCard.locator('[data-testid=tx-id]').textContent()).trim(), assetsTransferId)
|
|
1332
|
+
uutStatus(assetsTransferId, 'valid')
|
|
1333
|
+
await transferCard.locator('[data-testid=tx-status][data-status=confirmed]').waitFor({ timeout: 5_000 })
|
|
1334
|
+
ok('the digital asset transfer names both, carries the minimum, and is confirmed over the socket')
|
|
1335
|
+
|
|
1336
|
+
// A built transfer can wait only so long. Once the chain is past its last block, Submit
|
|
1337
|
+
// says so rather than posting something every node would refuse.
|
|
1338
|
+
ledger.block = 49
|
|
1339
|
+
await transferCard.locator('[data-testid=send-geeq]').check()
|
|
1340
|
+
await transferCard.locator('[data-testid=amount]').fill('2')
|
|
1341
|
+
await transferCard.locator('[data-testid=build-transfer]').click()
|
|
1342
|
+
await transferCard.locator('[data-testid=transfer-base64]').waitFor({ timeout: 20_000 })
|
|
1343
|
+
assert.equal(
|
|
1344
|
+
(await transferCard.locator('[data-testid=tx-valid-until]').textContent()).trim(),
|
|
1345
|
+
'58',
|
|
1346
|
+
'its target block, 50, plus 8',
|
|
1347
|
+
)
|
|
1348
|
+
sent = submitted.length
|
|
1349
|
+
ledger.block = 59
|
|
1350
|
+
await transferCard.locator('[data-testid=submit-transfer]').click()
|
|
1351
|
+
await transferCard.locator('[data-testid=tx-expired]').waitFor({ timeout: 10_000 })
|
|
1352
|
+
assert.equal(submitted.length, sent, 'nothing was posted')
|
|
1353
|
+
assert.ok(await transferCard.locator('[data-testid=submit-transfer]').isDisabled(), 'and Submit is off')
|
|
1354
|
+
ok('a transfer held past its last block is not sent, and says to build again')
|
|
1355
|
+
|
|
1356
|
+
// 14. The block explorer. The page takes a base64 refHash and the explorer is asked in hex.
|
|
1357
|
+
const explorerCard = page.locator('section.card').nth(8)
|
|
1358
|
+
const evidenceHex = Buffer.from(evidenceId, 'base64').toString('hex')
|
|
1359
|
+
indexed.set(evidenceHex, {
|
|
1360
|
+
refHash: evidenceHex,
|
|
1361
|
+
type: 'attestation',
|
|
1362
|
+
status: 'valid',
|
|
1363
|
+
block: 42,
|
|
1364
|
+
chainName: 'test',
|
|
1365
|
+
chainNumber: 1,
|
|
1366
|
+
from: 'GEEQ-SENDER-ADDRESS',
|
|
1367
|
+
senderType: 'coin',
|
|
1368
|
+
to: '',
|
|
1369
|
+
receiverType: '',
|
|
1370
|
+
publicKey: '',
|
|
1371
|
+
signature: '',
|
|
1372
|
+
createdAt: '2026-10-01T10:00:00.000Z',
|
|
1373
|
+
fee: '1',
|
|
1374
|
+
values: [],
|
|
1375
|
+
})
|
|
1376
|
+
await explorerCard.locator('[data-testid=explorer-url]').fill(base)
|
|
1377
|
+
await explorerCard.locator('[data-testid=explorer-refhash]').fill(evidenceId)
|
|
1378
|
+
await explorerCard.locator('[data-testid=explorer-lookup]').click()
|
|
1379
|
+
await explorerCard.locator('[data-testid=explorer-result]').waitFor({ timeout: 10_000 })
|
|
1380
|
+
assert.equal(explorerAsked.at(-1), evidenceHex, 'the explorer was asked in hex')
|
|
1381
|
+
assert.match(
|
|
1382
|
+
await explorerCard.locator('[data-testid=explorer-result]').textContent(),
|
|
1383
|
+
/valid[\s\S]*attestation[\s\S]*42 · chain 1[\s\S]*GEEQ-SENDER-ADDRESS/,
|
|
1384
|
+
'what the explorer returned is shown',
|
|
1385
|
+
)
|
|
1386
|
+
ok('a transaction is found in the explorer by its base64 refHash')
|
|
1387
|
+
|
|
1388
|
+
// Chromium logs the explorer's 500 for an unknown refHash; that answer is the point here.
|
|
1389
|
+
expectedConsoleErrors.push(/status of 500/)
|
|
1390
|
+
await explorerCard.locator('[data-testid=explorer-refhash]').fill(transferId)
|
|
1391
|
+
await explorerCard.locator('[data-testid=explorer-lookup]').click()
|
|
1392
|
+
await explorerCard.locator('[data-testid=explorer-not-found]').waitFor({ timeout: 10_000 })
|
|
1393
|
+
assert.equal(await explorerCard.locator('[data-testid=explorer-result]').count(), 0, 'no stale result')
|
|
1394
|
+
ok('a refHash the explorer does not have is reported as not found')
|
|
1395
|
+
|
|
1396
|
+
const askedSoFar = explorerAsked.length
|
|
1397
|
+
await explorerCard.locator('[data-testid=explorer-refhash]').fill(evidenceHex)
|
|
1398
|
+
await explorerCard.locator('[data-testid=explorer-lookup]').click()
|
|
1399
|
+
await explorerCard.locator('[data-testid=explorer-error]').waitFor({ timeout: 10_000 })
|
|
1400
|
+
assert.match(
|
|
1401
|
+
await explorerCard.locator('[data-testid=explorer-error]').textContent(),
|
|
1402
|
+
/32-byte hash/,
|
|
1403
|
+
'a hex refHash is refused, since it is not the base64 of 32 bytes',
|
|
1404
|
+
)
|
|
1405
|
+
assert.equal(explorerAsked.length, askedSoFar, 'and the explorer was not asked')
|
|
1406
|
+
ok('a refHash that is not base64 is refused before the explorer is asked')
|
|
1407
|
+
|
|
1408
|
+
// 9. Nothing broke along the way.
|
|
1409
|
+
for (const expected of expectedConsoleErrors) {
|
|
1410
|
+
const index = consoleErrors.findIndex((line) => expected.test(line))
|
|
1411
|
+
assert.notEqual(index, -1, `expected a console error matching ${expected}`)
|
|
1412
|
+
consoleErrors.splice(index, 1)
|
|
1413
|
+
}
|
|
1414
|
+
assert.deepEqual(consoleErrors, [], `console errors: ${consoleErrors.join(' | ')}`)
|
|
1415
|
+
ok('no console errors')
|
|
1416
|
+
assert.deepEqual(failedRequests, [], `failed requests: ${failedRequests.join(' | ')}`)
|
|
1417
|
+
ok('no failed network requests')
|
|
1418
|
+
|
|
1419
|
+
await browser.close()
|
|
1420
|
+
server.close()
|
|
1421
|
+
console.log(`\nAll ${n} browser checks passed.`)
|