@geeqdev/geeq-sdk 0.1.1 → 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/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/package.json +2 -2
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { blake3 } from '@noble/hashes/blake3';
|
|
3
|
+
import { GeeqChain, attest, createAttestationTx, getExplorerTransaction, initChain, initGeeq, keypairFromMnemonic, } from '../src/index';
|
|
4
|
+
import { NetActorStatus, NetActorType, vllResponse } from './fixtures/build-vll';
|
|
5
|
+
import { fakeTransport, networkError } from './fixtures/fake-transport';
|
|
6
|
+
// A standard BIP-39 test vector. Throwaway — never use it for real funds.
|
|
7
|
+
const MNEMONIC = 'legal winner thank year wave sausage worth useful legal winner thank yellow';
|
|
8
|
+
const ENDPOINT = 'https://10.0.0.1:2082';
|
|
9
|
+
const DIRECTORY = 'https://simulator.example.com';
|
|
10
|
+
let keypair;
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
await initGeeq();
|
|
13
|
+
keypair = await keypairFromMnemonic(MNEMONIC);
|
|
14
|
+
});
|
|
15
|
+
// The default chain is process-wide state; leaking it between tests would make failures
|
|
16
|
+
// depend on file order.
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
GeeqChain.reset();
|
|
19
|
+
});
|
|
20
|
+
async function failure(promise) {
|
|
21
|
+
let caught;
|
|
22
|
+
try {
|
|
23
|
+
await promise;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
caught = error;
|
|
27
|
+
}
|
|
28
|
+
if (caught === undefined)
|
|
29
|
+
throw new Error('expected the call to reject, but it resolved');
|
|
30
|
+
return caught;
|
|
31
|
+
}
|
|
32
|
+
function ledgerBody(overrides = {}) {
|
|
33
|
+
return vllResponse({
|
|
34
|
+
chainNumber: 1,
|
|
35
|
+
blockNumber: 41,
|
|
36
|
+
nodes: [{ ip: '10.0.0.1', nodeNumber: 3 }],
|
|
37
|
+
accounts: [{ accountId: keypair.accountId, kind: 'asset', nonce: 7 }],
|
|
38
|
+
...overrides,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** What the directory answers for `chains` then `nodes`. */
|
|
42
|
+
function directoryScript(nodes = [{ ip: '10.0.0.1' }], chains = [
|
|
43
|
+
{ id: 9, chainNumber: 1, name: 'test' },
|
|
44
|
+
], ledger = ledgerBody()) {
|
|
45
|
+
return (url) => {
|
|
46
|
+
if (url.includes('/admin/simulation/chains'))
|
|
47
|
+
return { body: JSON.stringify({ result: chains }) };
|
|
48
|
+
if (url.includes('/api/nodes')) {
|
|
49
|
+
return {
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
result: nodes.map((n) => ({
|
|
52
|
+
ip: n.ip,
|
|
53
|
+
port: 3000,
|
|
54
|
+
protocol: 'http',
|
|
55
|
+
systemId: 9,
|
|
56
|
+
publicKey: '',
|
|
57
|
+
type: n.type ?? 'active',
|
|
58
|
+
status: n.status ?? 'ready',
|
|
59
|
+
})),
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { body: ledger };
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function connect(script = { body: ledgerBody() }) {
|
|
67
|
+
const transport = fakeTransport(script);
|
|
68
|
+
return { transport, chain: GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport }) };
|
|
69
|
+
}
|
|
70
|
+
describe('GeeqChain.connect', () => {
|
|
71
|
+
// The ledger goes stale within a handful of blocks, so reading it at startup would
|
|
72
|
+
// only mean reading it again later. Connecting is bookkeeping, not I/O.
|
|
73
|
+
it('reads nothing from the chain', async () => {
|
|
74
|
+
const { transport, chain } = connect();
|
|
75
|
+
const geeq = await chain;
|
|
76
|
+
expect(transport.calls).toHaveLength(0);
|
|
77
|
+
expect(geeq.endpoint).toBe(ENDPOINT);
|
|
78
|
+
expect(geeq.node).toBeUndefined();
|
|
79
|
+
expect(geeq.chainNumber).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
it('knows the chain number up front when the caller declares one', async () => {
|
|
82
|
+
const transport = fakeTransport({ body: ledgerBody() });
|
|
83
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, chainNumber: 1, transport });
|
|
84
|
+
expect(geeq.chainNumber).toBe(1);
|
|
85
|
+
});
|
|
86
|
+
it('requires exactly one of discovery and endpoint', async () => {
|
|
87
|
+
const transport = fakeTransport({ body: ledgerBody() });
|
|
88
|
+
await expect(GeeqChain.connect({ penniesPerGeeq: 1000000000n, transport })).rejects.toMatchObject({
|
|
89
|
+
code: 'invalid-argument',
|
|
90
|
+
});
|
|
91
|
+
await expect(GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, discovery: DIRECTORY, transport })).rejects.toMatchObject({ code: 'invalid-argument' });
|
|
92
|
+
});
|
|
93
|
+
// Left to the network layer, each of these surfaces later as an unreachable node — a
|
|
94
|
+
// negative retries sends nothing, a zero timeout times everything out — which is the
|
|
95
|
+
// one thing it is not.
|
|
96
|
+
it.each([
|
|
97
|
+
['retries', { retries: -1 }],
|
|
98
|
+
['retries', { retries: 1.5 }],
|
|
99
|
+
['timeoutMs', { timeoutMs: 0 }],
|
|
100
|
+
['timeoutMs', { timeoutMs: Number.NaN }],
|
|
101
|
+
['ledgerMaxAgeMs', { ledgerMaxAgeMs: -1 }],
|
|
102
|
+
['nodePort', { nodePort: 0 }],
|
|
103
|
+
['nodePort', { nodePort: 70_000 }],
|
|
104
|
+
// What a cleared <input type="number" v-model.number> holds.
|
|
105
|
+
['nodePort', { nodePort: '' }],
|
|
106
|
+
['chainNumber', { chainNumber: 1.5 }],
|
|
107
|
+
['nodeNumber', { nodeNumber: -1 }],
|
|
108
|
+
])('refuses a %s of %j before reading anything', async (name, option) => {
|
|
109
|
+
const transport = fakeTransport({ body: ledgerBody() });
|
|
110
|
+
const error = await failure(GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport, ...option }));
|
|
111
|
+
expect(error).toMatchObject({ code: 'invalid-argument' });
|
|
112
|
+
expect(error.message).toContain(name);
|
|
113
|
+
expect(transport.calls).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('abort signals', () => {
|
|
117
|
+
/** A transport that answers after a few ms and, like fetch, rejects when its signal aborts. */
|
|
118
|
+
function slowTransport(body) {
|
|
119
|
+
const calls = [];
|
|
120
|
+
const transport = (url, init) => {
|
|
121
|
+
calls.push(url);
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const timer = setTimeout(() => resolve({ ok: true, status: 200, text: async () => body }), 5);
|
|
124
|
+
init?.signal?.addEventListener('abort', () => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
reject(init.signal?.reason);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
return Object.assign(transport, { calls });
|
|
131
|
+
}
|
|
132
|
+
it('sends nothing for a signal that is already aborted', async () => {
|
|
133
|
+
const transport = fakeTransport({ body: ledgerBody() });
|
|
134
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
135
|
+
const reason = new Error('user left');
|
|
136
|
+
await expect(geeq.getLedger({ signal: AbortSignal.abort(reason) })).rejects.toBe(reason);
|
|
137
|
+
await expect(geeq.getChainState({ account: keypair, signal: AbortSignal.abort(reason) })).rejects.toBe(reason);
|
|
138
|
+
expect(transport.calls).toHaveLength(0);
|
|
139
|
+
});
|
|
140
|
+
// The read is shared, so one caller abandoning it must not fail everyone else waiting.
|
|
141
|
+
it("stops one caller's wait on a shared read without failing the others", async () => {
|
|
142
|
+
const transport = slowTransport(ledgerBody());
|
|
143
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const reason = new Error('this caller left');
|
|
146
|
+
const leaving = geeq.getLedger({ signal: controller.signal });
|
|
147
|
+
const staying = geeq.getLedger();
|
|
148
|
+
controller.abort(reason);
|
|
149
|
+
// Settled together, so a regression fails the assertions rather than leaving one
|
|
150
|
+
// promise rejected and unobserved.
|
|
151
|
+
const [left, stayed] = await Promise.allSettled([leaving, staying]);
|
|
152
|
+
expect(left).toEqual({ status: 'rejected', reason });
|
|
153
|
+
expect(stayed).toMatchObject({ status: 'fulfilled', value: { blockNumber: 41 } });
|
|
154
|
+
expect(transport.calls).toHaveLength(1);
|
|
155
|
+
// The read ran on and filled the cache for whoever is next.
|
|
156
|
+
expect(geeq.ledger?.blockNumber).toBe(41);
|
|
157
|
+
});
|
|
158
|
+
it("rejects with the caller's own reason when it aborts mid-request", async () => {
|
|
159
|
+
const transport = slowTransport(ledgerBody());
|
|
160
|
+
const reason = new Error('navigated away');
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
const pending = getExplorerTransaction({
|
|
163
|
+
explorerUrl: 'https://blkexp.example.com',
|
|
164
|
+
refHash: Buffer.alloc(32, 7).toString('base64'),
|
|
165
|
+
transport,
|
|
166
|
+
signal: controller.signal,
|
|
167
|
+
});
|
|
168
|
+
controller.abort(reason);
|
|
169
|
+
await expect(pending).rejects.toBe(reason);
|
|
170
|
+
expect(transport.calls).toHaveLength(1);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe('node discovery', () => {
|
|
174
|
+
it('asks the directory for a chain and takes one of its active nodes', async () => {
|
|
175
|
+
const transport = fakeTransport(directoryScript());
|
|
176
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainNumber: 1, transport });
|
|
177
|
+
expect(transport.calls.map((c) => c.url)).toEqual([
|
|
178
|
+
'https://simulator.example.com/api/admin/simulation/chains',
|
|
179
|
+
'https://simulator.example.com/api/nodes?systemId=9',
|
|
180
|
+
]);
|
|
181
|
+
expect(geeq.chainNumber).toBe(1);
|
|
182
|
+
expect(geeq.endpoint).toBe('https://10.0.0.1:2082');
|
|
183
|
+
});
|
|
184
|
+
it('never reads a ledger while discovering', async () => {
|
|
185
|
+
const transport = fakeTransport(directoryScript());
|
|
186
|
+
await GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainNumber: 1, transport });
|
|
187
|
+
expect(transport.calls.some((c) => c.url.includes('type=vll'))).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
it('ignores the port the directory reports and uses the node port', async () => {
|
|
190
|
+
const transport = fakeTransport(directoryScript([{ ip: '10.0.0.7' }]));
|
|
191
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainNumber: 1, transport });
|
|
192
|
+
expect(geeq.endpoint).toBe('https://10.0.0.7:2082');
|
|
193
|
+
});
|
|
194
|
+
// A directory reached over http is a local or internal deployment, so its nodes are
|
|
195
|
+
// too; carrying the scheme over is what makes a dev harness work without special
|
|
196
|
+
// casing, and the port override is there for the same reason.
|
|
197
|
+
it('addresses nodes with the directory scheme, and an overridable port', async () => {
|
|
198
|
+
const transport = fakeTransport(directoryScript([{ ip: '127.0.0.1' }]));
|
|
199
|
+
const geeq = await GeeqChain.connect({
|
|
200
|
+
penniesPerGeeq: 1000000000n,
|
|
201
|
+
discovery: 'http://127.0.0.1:9100',
|
|
202
|
+
chainNumber: 1,
|
|
203
|
+
nodePort: 9100,
|
|
204
|
+
transport,
|
|
205
|
+
});
|
|
206
|
+
expect(geeq.endpoint).toBe('http://127.0.0.1:9100');
|
|
207
|
+
});
|
|
208
|
+
it('only ever picks a node the directory calls active and running', async () => {
|
|
209
|
+
const transport = fakeTransport(directoryScript([
|
|
210
|
+
{ ip: '10.0.0.1', type: 'listener' },
|
|
211
|
+
{ ip: '10.0.0.2', type: 'relay' },
|
|
212
|
+
{ ip: '10.0.0.3', type: 'active', status: 'stopped' },
|
|
213
|
+
{ ip: '10.0.0.4', type: 'active' },
|
|
214
|
+
]));
|
|
215
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainNumber: 1, transport });
|
|
216
|
+
expect(geeq.endpoint).toBe('https://10.0.0.4:2082');
|
|
217
|
+
expect(geeq.candidates).toHaveLength(4);
|
|
218
|
+
});
|
|
219
|
+
it('says so when no node can take work, and what it saw', async () => {
|
|
220
|
+
const transport = fakeTransport(directoryScript([
|
|
221
|
+
{ ip: '10.0.0.1', type: 'listener' },
|
|
222
|
+
{ ip: '10.0.0.2', type: 'active', status: 'stopped' },
|
|
223
|
+
]));
|
|
224
|
+
const error = await failure(GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainNumber: 1, transport }));
|
|
225
|
+
expect(error).toMatchObject({ code: 'node-unusable' });
|
|
226
|
+
expect(error.message).toMatch(/10\.0\.0\.1 \(listener\/ready\)/u);
|
|
227
|
+
expect(error.message).toMatch(/10\.0\.0\.2 \(active\/stopped\)/u);
|
|
228
|
+
});
|
|
229
|
+
describe('choosing a chain by name', () => {
|
|
230
|
+
const twoChains = [
|
|
231
|
+
{ id: 4, chainNumber: 7, name: 'stage' },
|
|
232
|
+
{ id: 9, chainNumber: 1, name: 'test' },
|
|
233
|
+
];
|
|
234
|
+
it('resolves the name to its protocol number', async () => {
|
|
235
|
+
const transport = fakeTransport(directoryScript(undefined, twoChains));
|
|
236
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainName: 'test', transport });
|
|
237
|
+
expect(geeq.chainNumber).toBe(1);
|
|
238
|
+
expect(geeq.chainName).toBe('test');
|
|
239
|
+
// systemId 9 is the directory's row for "test" — not its chain number.
|
|
240
|
+
expect(transport.calls[1]?.url).toMatch(/systemId=9$/u);
|
|
241
|
+
});
|
|
242
|
+
it('matches the way a person types it', async () => {
|
|
243
|
+
const transport = fakeTransport(directoryScript(undefined, twoChains));
|
|
244
|
+
const geeq = await GeeqChain.connect({
|
|
245
|
+
penniesPerGeeq: 1000000000n,
|
|
246
|
+
discovery: DIRECTORY,
|
|
247
|
+
chainName: ' TEST ',
|
|
248
|
+
transport,
|
|
249
|
+
});
|
|
250
|
+
expect(geeq.chainNumber).toBe(1);
|
|
251
|
+
});
|
|
252
|
+
it('lists what it has when the name is unknown', async () => {
|
|
253
|
+
const transport = fakeTransport(directoryScript(undefined, twoChains));
|
|
254
|
+
const error = await failure(GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainName: 'prod', transport }));
|
|
255
|
+
expect(error).toMatchObject({ code: 'invalid-argument' });
|
|
256
|
+
expect(error.message).toMatch(/no chain named "prod"/u);
|
|
257
|
+
expect(error.message).toMatch(/stage \(chain 7\), test \(chain 1\)/u);
|
|
258
|
+
});
|
|
259
|
+
it('refuses a name that identifies more than one chain', async () => {
|
|
260
|
+
const transport = fakeTransport(directoryScript(undefined, [
|
|
261
|
+
{ id: 1, chainNumber: 1, name: 'test' },
|
|
262
|
+
{ id: 2, chainNumber: 7, name: 'test' },
|
|
263
|
+
]));
|
|
264
|
+
const error = await failure(GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainName: 'test', transport }));
|
|
265
|
+
expect(error.message).toMatch(/does not identify one/u);
|
|
266
|
+
});
|
|
267
|
+
it('treats a name and a number together as an assertion', async () => {
|
|
268
|
+
const transport = fakeTransport(directoryScript(undefined, twoChains));
|
|
269
|
+
const error = await failure(GeeqChain.connect({
|
|
270
|
+
penniesPerGeeq: 1000000000n,
|
|
271
|
+
discovery: DIRECTORY,
|
|
272
|
+
chainName: 'test',
|
|
273
|
+
chainNumber: 7,
|
|
274
|
+
transport,
|
|
275
|
+
}));
|
|
276
|
+
expect(error.message).toMatch(/"test" is chain 1, but chainNumber was given as 7/u);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
it('needs a chain name or number when the directory lists several', async () => {
|
|
280
|
+
const transport = fakeTransport(directoryScript(undefined, [
|
|
281
|
+
{ id: 1, chainNumber: 1, name: 'test' },
|
|
282
|
+
{ id: 2, chainNumber: 7, name: 'stage' },
|
|
283
|
+
]));
|
|
284
|
+
const error = await failure(GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, transport }));
|
|
285
|
+
expect(error).toMatchObject({ code: 'invalid-argument' });
|
|
286
|
+
expect(error.message).toMatch(/chainName or chainNumber is required/u);
|
|
287
|
+
expect(error.message).toMatch(/test \(chain 1\), stage \(chain 7\)/u);
|
|
288
|
+
});
|
|
289
|
+
it('takes the only chain when there is just one', async () => {
|
|
290
|
+
const transport = fakeTransport(directoryScript());
|
|
291
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, transport });
|
|
292
|
+
expect(geeq.chainNumber).toBe(1);
|
|
293
|
+
});
|
|
294
|
+
it('reports a chain the directory does not carry', async () => {
|
|
295
|
+
const transport = fakeTransport(directoryScript());
|
|
296
|
+
const error = await failure(GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainNumber: 99, transport }));
|
|
297
|
+
expect(error.message).toMatch(/does not list chain 99/u);
|
|
298
|
+
});
|
|
299
|
+
it('rejects a directory that answers with something else', async () => {
|
|
300
|
+
const transport = fakeTransport({ body: '<!DOCTYPE html>' });
|
|
301
|
+
await expect(GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainNumber: 1, transport })).rejects.toMatchObject({ code: 'node-unusable' });
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
describe('the first ledger read', () => {
|
|
305
|
+
it('resolves the node number, which the directory cannot supply', async () => {
|
|
306
|
+
const { transport, chain } = connect();
|
|
307
|
+
const geeq = await chain;
|
|
308
|
+
await geeq.getLedger();
|
|
309
|
+
expect(transport.calls[0]?.url).toBe('https://10.0.0.1:2082/api/blocks?type=vll');
|
|
310
|
+
expect(geeq.node?.nodeNumber).toBe(3);
|
|
311
|
+
expect(geeq.node?.processing).toBe(true);
|
|
312
|
+
expect(geeq.chainNumber).toBe(1);
|
|
313
|
+
});
|
|
314
|
+
it('exposes where a transaction must be posted', async () => {
|
|
315
|
+
const geeq = await connect().chain;
|
|
316
|
+
await geeq.getLedger();
|
|
317
|
+
expect(geeq.node?.submitUrl).toBe('https://10.0.0.1:2082/api/blocks/process');
|
|
318
|
+
});
|
|
319
|
+
it('refuses a chain that is not the one the caller declared', async () => {
|
|
320
|
+
const transport = fakeTransport({ body: ledgerBody({ chainNumber: 7 }) });
|
|
321
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, chainNumber: 1, transport });
|
|
322
|
+
await expect(geeq.getLedger()).rejects.toMatchObject({ code: 'chain-mismatch' });
|
|
323
|
+
});
|
|
324
|
+
it('refuses when the endpoint turns out to be a listener', async () => {
|
|
325
|
+
const transport = fakeTransport({
|
|
326
|
+
body: ledgerBody({
|
|
327
|
+
nodes: [
|
|
328
|
+
{
|
|
329
|
+
ip: '10.0.0.1',
|
|
330
|
+
nodeNumber: 3,
|
|
331
|
+
type: NetActorType.NET_ACTOR_TYPE_ANCHOR_NODE,
|
|
332
|
+
status: NetActorStatus.NET_ACTOR_STATUS_LISTENING,
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
}),
|
|
336
|
+
});
|
|
337
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
338
|
+
await expect(geeq.getLedger()).rejects.toMatchObject({ code: 'node-unusable' });
|
|
339
|
+
});
|
|
340
|
+
it('builds a sibling endpoint on the node port, carrying only the scheme', async () => {
|
|
341
|
+
const geeq = await connect().chain;
|
|
342
|
+
expect(geeq.endpointFor('10.0.0.9')).toBe('https://10.0.0.9:2082');
|
|
343
|
+
});
|
|
344
|
+
it('resolves an endpoint given without a port', async () => {
|
|
345
|
+
const transport = fakeTransport({ body: ledgerBody() });
|
|
346
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: 'https://10.0.0.1', transport });
|
|
347
|
+
await geeq.getLedger();
|
|
348
|
+
expect(geeq.endpoint).toBe('https://10.0.0.1:2082');
|
|
349
|
+
expect(transport.calls[0]?.url).toBe('https://10.0.0.1:2082/api/blocks?type=vll');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
describe('ledger caching', () => {
|
|
353
|
+
it('serves a fresh ledger from cache rather than re-reading', async () => {
|
|
354
|
+
const { transport, chain } = connect();
|
|
355
|
+
const geeq = await chain;
|
|
356
|
+
await geeq.getLedger();
|
|
357
|
+
await geeq.getLedger();
|
|
358
|
+
expect(transport.calls).toHaveLength(1);
|
|
359
|
+
});
|
|
360
|
+
it('re-reads when the cache is disabled', async () => {
|
|
361
|
+
const { transport, chain } = connect();
|
|
362
|
+
const geeq = await chain;
|
|
363
|
+
await geeq.getLedger();
|
|
364
|
+
await geeq.getLedger({ maxAgeMs: 0 });
|
|
365
|
+
expect(transport.calls).toHaveLength(2);
|
|
366
|
+
});
|
|
367
|
+
it('re-reads on refresh', async () => {
|
|
368
|
+
const { transport, chain } = connect();
|
|
369
|
+
const geeq = await chain;
|
|
370
|
+
await geeq.getLedger();
|
|
371
|
+
await geeq.refresh();
|
|
372
|
+
expect(transport.calls).toHaveLength(2);
|
|
373
|
+
});
|
|
374
|
+
// Without single-flight, a page building several transactions at once pulls a whole
|
|
375
|
+
// ledger per build from an endpoint with no rate limiting whatsoever.
|
|
376
|
+
it('collapses concurrent reads into one request', async () => {
|
|
377
|
+
const { transport, chain } = connect();
|
|
378
|
+
const geeq = await chain;
|
|
379
|
+
await Promise.all([geeq.getLedger(), geeq.getLedger(), geeq.getLedger()]);
|
|
380
|
+
expect(transport.calls).toHaveLength(1);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
describe('transport failures', () => {
|
|
384
|
+
it('retries a transport failure once, then reports all four browser causes', async () => {
|
|
385
|
+
const transport = fakeTransport(new TypeError('Failed to fetch'));
|
|
386
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
387
|
+
const error = await failure(geeq.getLedger());
|
|
388
|
+
expect(transport.calls).toHaveLength(2);
|
|
389
|
+
expect(error).toMatchObject({ code: 'network-unreachable' });
|
|
390
|
+
expect(error.message).toMatch(/CORS/u);
|
|
391
|
+
expect(error.message).toMatch(/certificate/u);
|
|
392
|
+
expect(error.message).toMatch(/https and the node is http/u);
|
|
393
|
+
expect(error.message).toMatch(/down or the URL is wrong/u);
|
|
394
|
+
});
|
|
395
|
+
it('names the real cause instead, when the runtime supplies one', async () => {
|
|
396
|
+
const transport = fakeTransport(networkError('ECONNREFUSED'));
|
|
397
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
398
|
+
const error = await failure(geeq.getLedger());
|
|
399
|
+
expect(error.message).toMatch(/ECONNREFUSED/u);
|
|
400
|
+
expect(error.message).not.toMatch(/four things/u);
|
|
401
|
+
});
|
|
402
|
+
it('honours a retries override', async () => {
|
|
403
|
+
const transport = fakeTransport(new TypeError('Failed to fetch'));
|
|
404
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport, retries: 0 });
|
|
405
|
+
await failure(geeq.getLedger());
|
|
406
|
+
expect(transport.calls).toHaveLength(1);
|
|
407
|
+
});
|
|
408
|
+
it('does not retry a response — the node has decided', async () => {
|
|
409
|
+
const transport = fakeTransport({
|
|
410
|
+
status: 503,
|
|
411
|
+
body: JSON.stringify({ error: 'x', details: 'Block chain is stopped' }),
|
|
412
|
+
});
|
|
413
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
414
|
+
const error = await failure(geeq.getLedger());
|
|
415
|
+
expect(transport.calls).toHaveLength(1);
|
|
416
|
+
expect(error).toMatchObject({
|
|
417
|
+
code: 'node-error',
|
|
418
|
+
status: 503,
|
|
419
|
+
nodeDetails: 'Block chain is stopped',
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
it.each([
|
|
423
|
+
['an HTML login page', '<!DOCTYPE html>'],
|
|
424
|
+
['JSON without a result field', JSON.stringify({ ok: true })],
|
|
425
|
+
['a result that is not a string', JSON.stringify({ result: 42 })],
|
|
426
|
+
['an empty result', JSON.stringify({ result: '' })],
|
|
427
|
+
])('rejects %s as an unreadable ledger', async (_label, body) => {
|
|
428
|
+
const transport = fakeTransport({ body });
|
|
429
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
430
|
+
await expect(geeq.getLedger()).rejects.toMatchObject({ code: 'ledger-unreadable' });
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
describe('getChainState', () => {
|
|
434
|
+
it('derives all four values from the ledger', async () => {
|
|
435
|
+
const geeq = await connect().chain;
|
|
436
|
+
const state = await geeq.getChainState({ account: keypair });
|
|
437
|
+
expect(state).toMatchObject({
|
|
438
|
+
chainNumber: 1,
|
|
439
|
+
blockNumber: 42, // the ledger's 41, plus one
|
|
440
|
+
ledgerBlockNumber: 41,
|
|
441
|
+
nodeNumber: 3,
|
|
442
|
+
nonce: 7,
|
|
443
|
+
nodeUrl: ENDPOINT,
|
|
444
|
+
});
|
|
445
|
+
expect(state.metadataLimits).toEqual({ maxEntries: 4, maxBytesPerEntry: 64 });
|
|
446
|
+
});
|
|
447
|
+
it('tells an unfunded account to get funded', async () => {
|
|
448
|
+
const transport = fakeTransport({ body: ledgerBody({ accounts: [] }) });
|
|
449
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
450
|
+
const error = await failure(geeq.getChainState({ account: keypair }));
|
|
451
|
+
expect(error).toMatchObject({ code: 'account-not-found' });
|
|
452
|
+
expect(error.message).toMatch(/received coins/u);
|
|
453
|
+
});
|
|
454
|
+
const kinds = [
|
|
455
|
+
['hl', /hash-lock/u],
|
|
456
|
+
['micro', /micro-coin/u],
|
|
457
|
+
['asset-empty', /subtype could not be read/u],
|
|
458
|
+
];
|
|
459
|
+
it.each(kinds)('refuses a %s record with an explanation', async (kind, pattern) => {
|
|
460
|
+
const transport = fakeTransport({
|
|
461
|
+
body: ledgerBody({ accounts: [{ accountId: keypair.accountId, kind }] }),
|
|
462
|
+
});
|
|
463
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
464
|
+
const error = await failure(geeq.getChainState({ account: keypair }));
|
|
465
|
+
expect(error).toMatchObject({ code: 'account-not-attestable' });
|
|
466
|
+
expect(error.message).toMatch(pattern);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
describe('createAttestation', () => {
|
|
470
|
+
it('reads the ledger at build time, not before', async () => {
|
|
471
|
+
const { transport, chain } = connect();
|
|
472
|
+
const geeq = await chain;
|
|
473
|
+
expect(transport.calls).toHaveLength(0);
|
|
474
|
+
await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
475
|
+
expect(transport.calls).toHaveLength(1);
|
|
476
|
+
});
|
|
477
|
+
it('produces the same bytes as passing the four values by hand', async () => {
|
|
478
|
+
const geeq = await connect().chain;
|
|
479
|
+
const content = new TextEncoder().encode('hello');
|
|
480
|
+
const derived = await geeq.createAttestation({ keypair, content });
|
|
481
|
+
const explicit = await createAttestationTx({
|
|
482
|
+
keypair,
|
|
483
|
+
attestHash: blake3(content, { dkLen: 32 }),
|
|
484
|
+
chainNumber: 1,
|
|
485
|
+
blockNumber: 42,
|
|
486
|
+
nodeNumber: 3,
|
|
487
|
+
nonce: 7,
|
|
488
|
+
});
|
|
489
|
+
expect(derived.transaction).toBe(explicit.transaction);
|
|
490
|
+
expect(derived.payload).toBe(explicit.payload);
|
|
491
|
+
});
|
|
492
|
+
it('carries the node it must be submitted to', async () => {
|
|
493
|
+
const geeq = await connect().chain;
|
|
494
|
+
const tx = await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
495
|
+
expect(tx.nodeUrl).toBe(ENDPOINT);
|
|
496
|
+
});
|
|
497
|
+
it('requires exactly one of content and attestHash', async () => {
|
|
498
|
+
const geeq = await connect().chain;
|
|
499
|
+
await expect(geeq.createAttestation({ keypair })).rejects.toMatchObject({
|
|
500
|
+
code: 'invalid-argument',
|
|
501
|
+
});
|
|
502
|
+
await expect(geeq.createAttestation({
|
|
503
|
+
keypair,
|
|
504
|
+
content: new Uint8Array([1]),
|
|
505
|
+
attestHash: new Uint8Array(32),
|
|
506
|
+
})).rejects.toMatchObject({ code: 'invalid-argument' });
|
|
507
|
+
});
|
|
508
|
+
it('checks metadata against the chain limits before signing', async () => {
|
|
509
|
+
const transport = fakeTransport({ body: ledgerBody({ metadataVarNum: 1 }) });
|
|
510
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
511
|
+
await expect(geeq.createAttestation({
|
|
512
|
+
keypair,
|
|
513
|
+
content: new Uint8Array([1]),
|
|
514
|
+
metadata: ['one', 'two'],
|
|
515
|
+
})).rejects.toMatchObject({ code: 'metadata-too-large' });
|
|
516
|
+
});
|
|
517
|
+
it('counts metadata in UTF-8 bytes, not characters', async () => {
|
|
518
|
+
const transport = fakeTransport({ body: ledgerBody({ metadataVarBytes: 4 }) });
|
|
519
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
520
|
+
// Two emoji are 8 bytes once encoded, over a 4-byte limit, despite "looking" short.
|
|
521
|
+
await expect(geeq.createAttestation({ keypair, content: new Uint8Array([1]), metadata: ['🙂🙂'] })).rejects.toMatchObject({ code: 'metadata-too-large' });
|
|
522
|
+
});
|
|
523
|
+
it('works end to end through discovery', async () => {
|
|
524
|
+
const transport = fakeTransport(directoryScript());
|
|
525
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainNumber: 1, transport });
|
|
526
|
+
const tx = await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
527
|
+
expect(tx.nodeUrl).toBe('https://10.0.0.1:2082');
|
|
528
|
+
expect(tx.chainState).toMatchObject({ chainNumber: 1, nodeNumber: 3, nonce: 7 });
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
describe('the nonce guard', () => {
|
|
532
|
+
it('refuses a second attestation for one account against one block', async () => {
|
|
533
|
+
const geeq = await connect().chain;
|
|
534
|
+
await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
535
|
+
const error = await failure(geeq.createAttestation({ keypair, content: new Uint8Array([2]) }));
|
|
536
|
+
expect(error).toMatchObject({ code: 'nonce-collision' });
|
|
537
|
+
expect(error.message).toMatch(/both rejected, not just the second/u);
|
|
538
|
+
});
|
|
539
|
+
it('allows the next one once the chain has moved on', async () => {
|
|
540
|
+
let block = 41;
|
|
541
|
+
const transport = fakeTransport(() => ({ body: ledgerBody({ blockNumber: block }) }));
|
|
542
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
543
|
+
await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
544
|
+
block = 42;
|
|
545
|
+
await geeq.refresh();
|
|
546
|
+
await expect(geeq.createAttestation({ keypair, content: new Uint8Array([2]) })).resolves.toMatchObject({ nodeUrl: ENDPOINT });
|
|
547
|
+
});
|
|
548
|
+
// The cache is a few seconds wide, so a caller who waited out the block interval and
|
|
549
|
+
// tried again would otherwise be told to wait for a block that has already passed.
|
|
550
|
+
it('looks again before refusing, in case the cached ledger is simply behind', async () => {
|
|
551
|
+
let block = 41;
|
|
552
|
+
const transport = fakeTransport(() => ({ body: ledgerBody({ blockNumber: block }) }));
|
|
553
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
554
|
+
await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
555
|
+
block = 42;
|
|
556
|
+
// No explicit refresh: the collision check re-reads on its own.
|
|
557
|
+
const second = await geeq.createAttestation({ keypair, content: new Uint8Array([2]) });
|
|
558
|
+
expect(second.chainState.blockNumber).toBe(43);
|
|
559
|
+
});
|
|
560
|
+
it('still refuses when the chain really has not moved', async () => {
|
|
561
|
+
const { transport, chain } = connect();
|
|
562
|
+
const geeq = await chain;
|
|
563
|
+
await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
564
|
+
const before = transport.calls.length;
|
|
565
|
+
const error = await failure(geeq.createAttestation({ keypair, content: new Uint8Array([2]) }));
|
|
566
|
+
expect(error).toMatchObject({ code: 'nonce-collision' });
|
|
567
|
+
// It did look again rather than refusing from cache alone.
|
|
568
|
+
expect(transport.calls.length).toBeGreaterThan(before);
|
|
569
|
+
});
|
|
570
|
+
it('steps aside when the caller names a nonce deliberately', async () => {
|
|
571
|
+
const geeq = await connect().chain;
|
|
572
|
+
await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
573
|
+
const second = await geeq.createAttestation({
|
|
574
|
+
keypair,
|
|
575
|
+
content: new Uint8Array([2]),
|
|
576
|
+
nonce: 8,
|
|
577
|
+
});
|
|
578
|
+
expect(second.chainState.nonce).toBe(8);
|
|
579
|
+
});
|
|
580
|
+
it('can be reset explicitly', async () => {
|
|
581
|
+
const geeq = await connect().chain;
|
|
582
|
+
await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
583
|
+
geeq.forgetNonces();
|
|
584
|
+
await expect(geeq.createAttestation({ keypair, content: new Uint8Array([2]) })).resolves.toBeDefined();
|
|
585
|
+
});
|
|
586
|
+
it('takes back the nonce of a build that will not be sent, and builds again on it', async () => {
|
|
587
|
+
const geeq = await connect().chain;
|
|
588
|
+
const unsent = await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
589
|
+
geeq.discard(unsent);
|
|
590
|
+
const rebuilt = await geeq.createAttestation({ keypair, content: new Uint8Array([2]) });
|
|
591
|
+
expect(rebuilt.chainState.nonce).toBe(unsent.chainState.nonce);
|
|
592
|
+
});
|
|
593
|
+
it('takes back only the build it is given', async () => {
|
|
594
|
+
const geeq = await connect().chain;
|
|
595
|
+
const first = await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
596
|
+
geeq.discard(first);
|
|
597
|
+
await geeq.createAttestation({ keypair, content: new Uint8Array([2]) });
|
|
598
|
+
// Discarding the first again must not release the second's claim.
|
|
599
|
+
geeq.discard(first);
|
|
600
|
+
await expect(geeq.createAttestation({ keypair, content: new Uint8Array([3]) })).rejects.toMatchObject({ code: 'nonce-collision' });
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
describe('how long a built transaction lasts', () => {
|
|
604
|
+
it('carries the last block a node takes it at: its target plus 8', async () => {
|
|
605
|
+
const geeq = await connect().chain;
|
|
606
|
+
const tx = await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
607
|
+
expect(tx.chainState.blockNumber).toBe(42);
|
|
608
|
+
expect(tx.validUntilBlock).toBe(50);
|
|
609
|
+
});
|
|
610
|
+
// A node takes a target block within [N-8, N+1] of its own last block N.
|
|
611
|
+
it('is expired only once the chain is past that block, read afresh', async () => {
|
|
612
|
+
let block = 41;
|
|
613
|
+
const transport = fakeTransport(() => ({ body: ledgerBody({ blockNumber: block }) }));
|
|
614
|
+
const geeq = await GeeqChain.connect({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
615
|
+
const tx = await geeq.createAttestation({ keypair, content: new Uint8Array([1]) });
|
|
616
|
+
block = 50;
|
|
617
|
+
await expect(geeq.isExpired(tx)).resolves.toBe(false);
|
|
618
|
+
// No refresh in between: from cache it would still be block 50.
|
|
619
|
+
block = 51;
|
|
620
|
+
await expect(geeq.isExpired(tx)).resolves.toBe(true);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
describe('the default chain', () => {
|
|
624
|
+
it('names the call to make when there is no chain yet', () => {
|
|
625
|
+
expect(() => GeeqChain.current()).toThrow(expect.objectContaining({ code: 'not-initialized' }));
|
|
626
|
+
expect(() => GeeqChain.current()).toThrow(/GeeqChain\.init/u);
|
|
627
|
+
});
|
|
628
|
+
it('is installed by initChain and used by attest', async () => {
|
|
629
|
+
const transport = fakeTransport(directoryScript());
|
|
630
|
+
await initChain({ penniesPerGeeq: 1000000000n, discovery: DIRECTORY, chainNumber: 1, transport });
|
|
631
|
+
expect(GeeqChain.initialized).toBe(true);
|
|
632
|
+
await expect(attest({ keypair, content: new Uint8Array([1]) })).resolves.toMatchObject({
|
|
633
|
+
nodeUrl: 'https://10.0.0.1:2082',
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
it('is not installed by connect', async () => {
|
|
637
|
+
await connect().chain;
|
|
638
|
+
expect(GeeqChain.initialized).toBe(false);
|
|
639
|
+
});
|
|
640
|
+
it('stands down when the chain holding the slot is disposed', async () => {
|
|
641
|
+
const transport = fakeTransport({ body: ledgerBody() });
|
|
642
|
+
const geeq = await initChain({ penniesPerGeeq: 1000000000n, endpoint: ENDPOINT, transport });
|
|
643
|
+
geeq.dispose();
|
|
644
|
+
expect(GeeqChain.initialized).toBe(false);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
//# sourceMappingURL=chain.test.js.map
|