@geeqdev/geeq-sdk 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.idea/geeq_sdk.iml +12 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/vcs.xml +6 -0
  4. package/.nvmrc +1 -0
  5. package/CONTRIBUTING.md +20 -0
  6. package/example/.env.example +4 -0
  7. package/example/README.md +304 -0
  8. package/example/USAGE.md +74 -0
  9. package/example/index.html +12 -0
  10. package/example/package-lock.json +1557 -0
  11. package/example/package.json +25 -0
  12. package/example/scripts/browser-check.mjs +1421 -0
  13. package/example/scripts/match-derivation.mjs +65 -0
  14. package/example/src/App.vue +138 -0
  15. package/example/src/components/AuthorityPanel.vue +277 -0
  16. package/example/src/components/ChainPanel.vue +267 -0
  17. package/example/src/components/ChainState.vue +59 -0
  18. package/example/src/components/CopyButton.vue +25 -0
  19. package/example/src/components/CreateAssetPanel.vue +313 -0
  20. package/example/src/components/DigitalAssetsPanel.vue +68 -0
  21. package/example/src/components/EvidencePanel.vue +396 -0
  22. package/example/src/components/ExplorerPanel.vue +119 -0
  23. package/example/src/components/KeypairPanel.vue +182 -0
  24. package/example/src/components/PhrasePanel.vue +105 -0
  25. package/example/src/components/SocketWarning.vue +18 -0
  26. package/example/src/components/StepGate.vue +24 -0
  27. package/example/src/components/TransferPanel.vue +416 -0
  28. package/example/src/components/TxDeadline.vue +19 -0
  29. package/example/src/components/TxFee.vue +31 -0
  30. package/example/src/components/TxStatus.vue +105 -0
  31. package/example/src/composables/useSocketConnection.ts +80 -0
  32. package/example/src/env.d.ts +12 -0
  33. package/example/src/main.ts +5 -0
  34. package/example/src/pennies.ts +7 -0
  35. package/example/src/style.css +149 -0
  36. package/example/tsconfig.json +19 -0
  37. package/example/vite.config.ts +26 -0
  38. package/jest.config.js +18 -0
  39. package/package.json +1 -5
  40. package/scripts/copy-wasm.mjs +21 -0
  41. package/scripts/smoke.mjs +382 -0
  42. package/src/address/record-type.ts +77 -0
  43. package/src/address/wallet-address.ts +105 -0
  44. package/src/amount.ts +143 -0
  45. package/src/assert.ts +30 -0
  46. package/src/capnp/README.md +15 -0
  47. package/src/capnp/adminNetTx.capnp.ts +66 -0
  48. package/src/capnp/appLayBlock.capnp.ts +26 -0
  49. package/src/capnp/appLayLedger.capnp.ts +35 -0
  50. package/src/capnp/appLayRec.capnp.ts +159 -0
  51. package/src/capnp/chainStateView.capnp.ts +49 -0
  52. package/src/capnp/coinAcctRec.capnp.ts +193 -0
  53. package/src/capnp/genesisBlock.capnp.ts +56 -0
  54. package/src/capnp/hubContentStmt.capnp.ts +59 -0
  55. package/src/capnp/misc.capnp.ts +19 -0
  56. package/src/capnp/netActorRec.capnp.ts +64 -0
  57. package/src/capnp/nodeContentStmt.capnp.ts +62 -0
  58. package/src/capnp/nodePayloadList.capnp.ts +28 -0
  59. package/src/capnp/nodeTxList.capnp.ts +37 -0
  60. package/src/capnp/paramAcctRec.capnp.ts +669 -0
  61. package/src/capnp/payload.capnp.ts +261 -0
  62. package/src/capnp/proxyAcctRec.capnp.ts +259 -0
  63. package/src/capnp/relayContentStmt.capnp.ts +85 -0
  64. package/src/capnp/sysAcctRec.capnp.ts +47 -0
  65. package/src/capnp/userAppTx.capnp.ts +269 -0
  66. package/src/capnp/userValTx.capnp.ts +1999 -0
  67. package/src/capnp/valLayBlock.capnp.ts +93 -0
  68. package/src/capnp/valLayLedger.capnp.ts +80 -0
  69. package/src/chain/geeq-chain.ts +1094 -0
  70. package/src/chain/geeq-node.ts +105 -0
  71. package/src/chain/nonce-guard.ts +73 -0
  72. package/src/chain/select.ts +149 -0
  73. package/src/crypto/account-id.ts +83 -0
  74. package/src/crypto/keypair.ts +221 -0
  75. package/src/crypto/mnemonic.ts +69 -0
  76. package/src/encoding/base64.ts +58 -0
  77. package/src/encoding/hex.ts +42 -0
  78. package/src/errors.ts +122 -0
  79. package/src/index.ts +195 -0
  80. package/src/ledger/coin-accounts.ts +73 -0
  81. package/src/ledger/decode.ts +147 -0
  82. package/src/ledger/geeq-ledger.ts +184 -0
  83. package/src/ledger/net-actors.ts +129 -0
  84. package/src/ledger/nft-records.ts +49 -0
  85. package/src/ledger/params.ts +82 -0
  86. package/src/ledger/proxy-accounts.ts +51 -0
  87. package/src/net/discovery.ts +231 -0
  88. package/src/net/explorer.ts +123 -0
  89. package/src/net/read-ledger.ts +58 -0
  90. package/src/net/routes.ts +79 -0
  91. package/src/net/transport.ts +221 -0
  92. package/src/net/websocket.ts +94 -0
  93. package/src/runtime/bindings.ts +38 -0
  94. package/src/runtime/init.ts +49 -0
  95. package/src/runtime/wasm-loader.browser.ts +22 -0
  96. package/src/runtime/wasm-loader.node.ts +16 -0
  97. package/src/socket/geeq-socket.ts +827 -0
  98. package/src/socket/tracked-tx.ts +319 -0
  99. package/src/socket/tx-status.ts +131 -0
  100. package/src/tx/attestation.ts +222 -0
  101. package/src/tx/content.ts +82 -0
  102. package/src/tx/encode.ts +107 -0
  103. package/src/tx/expiry.ts +11 -0
  104. package/src/tx/fee.ts +31 -0
  105. package/src/tx/metadata.ts +100 -0
  106. package/src/tx/nft.ts +108 -0
  107. package/src/tx/proxy.ts +156 -0
  108. package/src/tx/transfer.ts +162 -0
  109. package/src/types.ts +503 -0
  110. package/src/wasm/README.md +22 -0
  111. package/src/wasm/browser/geeq_core.d.ts +1224 -0
  112. package/src/wasm/browser/geeq_core.js +6014 -0
  113. package/src/wasm/browser/geeq_core_bg.wasm +0 -0
  114. package/src/wasm/browser/geeq_core_bg.wasm.d.ts +447 -0
  115. package/src/wasm/nodejs/geeq_core.d.ts +753 -0
  116. package/src/wasm/nodejs/geeq_core.js +6016 -0
  117. package/src/wasm/nodejs/geeq_core_bg.wasm +0 -0
  118. package/src/wasm/nodejs/geeq_core_bg.wasm.d.ts +447 -0
  119. package/src/wasm/nodejs/package.json +3 -0
  120. package/tsconfig.build.json +8 -0
  121. package/tsconfig.json +20 -0
  122. package/tsup.config.ts +60 -0
@@ -0,0 +1,147 @@
1
+ import { Message } from 'capnp-ts'
2
+ import { ValLayLedger as CapnpValLayLedger } from '../capnp/valLayLedger.capnp'
3
+
4
+ import { MAX_UINT32 } from '../assert'
5
+ import { fromBase64, toBase64 } from '../encoding/base64'
6
+ import { GeeqError, ledgerUnreadable } from '../errors'
7
+ import type { CoinAccountFacts } from './coin-accounts'
8
+ import { readCoinAccount } from './coin-accounts'
9
+ import { GeeqLedger } from './geeq-ledger'
10
+ import type { GeeqLedgerNode } from './net-actors'
11
+ import { isProcessing, toNodeRole, toNodeStatus } from './net-actors'
12
+ import { readGeeqNorm, readMetadataLimits, readMinimumTransfer } from './params'
13
+ import type { ProxyAccountFacts } from './proxy-accounts'
14
+ import { readProxyAccount } from './proxy-accounts'
15
+
16
+ export interface DecodeLedgerOptions {
17
+ /** The node this came from, carried through onto the snapshot. */
18
+ readonly nodeUrl?: string | undefined
19
+ /** Overrides the read timestamp. For deterministic tests. */
20
+ readonly readAt?: number | undefined
21
+ }
22
+
23
+ /**
24
+ * Decode a validation layer ledger.
25
+ *
26
+ * `source` is what a node's `result` field holds: base64, or the bytes behind it. The
27
+ * message is Cap'n Proto **packed**, which is what the node writes with
28
+ * `toPackedArrayBuffer()` — not the flattened form used for signing.
29
+ *
30
+ * Everything is read in this one pass and the message is then dropped, so the caller
31
+ * gets plain values with no traversal budget attached. See {@link GeeqLedger}.
32
+ */
33
+ export function decodeValidationLedger(
34
+ source: Uint8Array | ArrayBuffer | string,
35
+ options: DecodeLedgerOptions = {},
36
+ ): GeeqLedger {
37
+ const bytes =
38
+ typeof source === 'string'
39
+ ? fromBase64(source, 'The ledger')
40
+ : source instanceof ArrayBuffer
41
+ ? new Uint8Array(source)
42
+ : source
43
+
44
+ if (bytes.length === 0) {
45
+ throw ledgerUnreadable(
46
+ 'The ledger is empty. A node reports this before it has committed its first ' +
47
+ 'block and while it is resyncing — wait and retry, or read from another node ' +
48
+ 'on the same chain.',
49
+ )
50
+ }
51
+
52
+ let ledger
53
+ try {
54
+ ledger = new Message(bytes, true).getRoot(CapnpValLayLedger)
55
+ } catch (cause) {
56
+ throw ledgerUnreadable(
57
+ `Could not decode ${bytes.length} bytes as a packed Cap'n Proto ValLayLedger. ` +
58
+ 'The node is probably running a schema version this SDK does not know — ' +
59
+ 'upgrade @geeqdev/geeq-sdk, or read the chain state yourself.',
60
+ cause,
61
+ )
62
+ }
63
+
64
+ try {
65
+ const chainNumber = ledger.getChainNum()
66
+ const blockNumber = ledger.getBlockNum()
67
+
68
+ if (blockNumber >= MAX_UINT32) {
69
+ // Every transaction targets blockNumber + 1, which would overflow the uint32 the
70
+ // protocol encodes it in. Refuse rather than silently wrap to zero.
71
+ throw ledgerUnreadable(
72
+ `The ledger reports block ${blockNumber}, the maximum a uint32 can hold, so no ` +
73
+ 'transaction can target the next block. The ledger is almost certainly corrupt.',
74
+ )
75
+ }
76
+
77
+ const nodes: GeeqLedgerNode[] = ledger
78
+ .getNetActorList()
79
+ .getNetActorRecs()
80
+ .toArray()
81
+ .map((record) => {
82
+ const role = toNodeRole(record.getNetActorType())
83
+ const status = toNodeStatus(record.getRecStatus())
84
+ const retiresAtBlock = record.getBlockNumRemove()
85
+ return Object.freeze({
86
+ nodeNumber: record.getNodeNumSelf(),
87
+ host: record.getIpAddr(),
88
+ port: record.getPortNum(),
89
+ role,
90
+ status,
91
+ processing: isProcessing(role, status),
92
+ retiresAtBlock,
93
+ // The node's own guard compares its blockNumRemove against its current block
94
+ // plus two, and the ledger's block is that current block.
95
+ retiring: retiresAtBlock < blockNumber + 2,
96
+ })
97
+ })
98
+
99
+ const params = ledger.getParamAcctList().getParamAcctRecs().toArray()
100
+
101
+ // A plausibility gate. Packed capnp can "succeed" on garbage and yield a message of
102
+ // zeros, which would otherwise surface much later as chain 0, block 0 and a missing
103
+ // account. A real ledger always carries net actors and parameters.
104
+ if (nodes.length === 0 || params.length === 0) {
105
+ throw ledgerUnreadable(
106
+ `The ledger decoded but does not look like one: chain ${chainNumber}, block ` +
107
+ `${blockNumber}, ${nodes.length} net actor records, ${params.length} parameter ` +
108
+ 'records. A real ledger always has at least one of each, so this URL is ' +
109
+ 'probably not a Geeq node.',
110
+ )
111
+ }
112
+
113
+ const accounts = new Map<string, CoinAccountFacts>()
114
+ const coinAccounts = ledger.getCoinAcctRecs()
115
+ for (let index = 0; index < coinAccounts.getLength(); index += 1) {
116
+ const record = coinAccounts.get(index)
117
+ const accountId = new Uint8Array(record.getAcctNumVal().toArrayBuffer())
118
+ accounts.set(toBase64(accountId), readCoinAccount(record))
119
+ }
120
+
121
+ const proxies = new Map<string, ProxyAccountFacts>()
122
+ for (const record of ledger.getProxyAcctRecs().toArray()) {
123
+ const accountId = new Uint8Array(record.getAcctNumVal().toArrayBuffer())
124
+ proxies.set(toBase64(accountId), readProxyAccount(record))
125
+ }
126
+
127
+ return new GeeqLedger({
128
+ chainNumber,
129
+ blockNumber,
130
+ nodes,
131
+ metadataLimits: readMetadataLimits(params),
132
+ minimumTransfer: readMinimumTransfer(params),
133
+ geeqNorm: readGeeqNorm(params),
134
+ accounts,
135
+ proxies,
136
+ readAt: options.readAt ?? Date.now(),
137
+ nodeUrl: options.nodeUrl,
138
+ })
139
+ } catch (cause) {
140
+ if (cause instanceof GeeqError) throw cause
141
+ throw ledgerUnreadable(
142
+ 'The ledger decoded but its contents could not be read. The node is probably ' +
143
+ 'running a schema version this SDK does not know.',
144
+ cause,
145
+ )
146
+ }
147
+ }
@@ -0,0 +1,184 @@
1
+ import { resolveAccountId } from '../crypto/account-id'
2
+ import type { AccountRef } from '../crypto/account-id'
3
+ import { fromBase64, toBase64 } from '../encoding/base64'
4
+ import type { GeeqMetadataLimits } from '../types'
5
+ import type { CoinAccountFacts, GeeqCoinAccount } from './coin-accounts'
6
+ import type { GeeqLedgerNode } from './net-actors'
7
+ import type { GeeqProxyAccount, ProxyAccountFacts } from './proxy-accounts'
8
+
9
+ /** @internal — built by {@link decodeValidationLedger}, never by hand. */
10
+ export interface GeeqLedgerInit {
11
+ readonly chainNumber: number
12
+ readonly blockNumber: number
13
+ readonly nodes: readonly GeeqLedgerNode[]
14
+ readonly metadataLimits: GeeqMetadataLimits | undefined
15
+ readonly minimumTransfer: bigint | undefined
16
+ readonly geeqNorm: bigint | undefined
17
+ readonly accounts: ReadonlyMap<string, CoinAccountFacts>
18
+ readonly proxies: ReadonlyMap<string, ProxyAccountFacts>
19
+ readonly readAt: number
20
+ readonly nodeUrl: string | undefined
21
+ }
22
+
23
+ /**
24
+ * One validation layer ledger, decoded into plain values.
25
+ *
26
+ * This is a snapshot, not a view: the Cap'n Proto `Message` it came from is released
27
+ * the moment decoding finishes. That is deliberate and load-bearing. capnp-ts counts a
28
+ * traversal budget per `Message` and never resets it, so a retained handle rescanned on
29
+ * every transaction eventually throws `PTR_TRAVERSAL_LIMIT_EXCEEDED`; and reading a
30
+ * malformed union arm mutates the message in place. Neither can happen to a snapshot.
31
+ *
32
+ * It holds no WebAssembly handle, so unlike a keypair it needs no `dispose()`.
33
+ */
34
+ export class GeeqLedger {
35
+ /** The ledger's own chain number. A transaction must name exactly this. */
36
+ readonly chainNumber: number
37
+ /** N — the last block committed to this ledger. */
38
+ readonly blockNumber: number
39
+ /** N + 1 — the block a transaction built now should ask for. */
40
+ readonly targetBlockNumber: number
41
+ /** Every net actor, in ledger order. */
42
+ readonly nodes: readonly GeeqLedgerNode[]
43
+ /** Undefined when this chain publishes no metadata parameters. */
44
+ readonly metadataLimits: GeeqMetadataLimits | undefined
45
+ /**
46
+ * The smallest transfer this chain accepts, in pennies.
47
+ *
48
+ * Undefined when the chain does not publish the parameters it is derived from.
49
+ */
50
+ readonly minimumTransfer: bigint | undefined
51
+ /**
52
+ * The chain's `geeqNorm` parameter, which the minimum transfer and every fee are
53
+ * multiples of. Undefined when the chain does not publish it.
54
+ */
55
+ readonly geeqNorm: bigint | undefined
56
+ /** `Date.now()` when this was read, so callers can judge staleness. */
57
+ readonly readAt: number
58
+ /** The node it was read from, when it came from one. */
59
+ readonly nodeUrl: string | undefined
60
+
61
+ // Deliberately a TypeScript `private`, not a `#private` field. A `#private` is
62
+ // unreachable through a Proxy, which breaks Vue's ref() — the same trap documented
63
+ // on Keypair in src/crypto/keypair.ts. Non-enumerable so it stays out of logs and
64
+ // structured clones, since on a busy chain it is the largest thing here.
65
+ private readonly accounts!: ReadonlyMap<string, CoinAccountFacts>
66
+ private readonly proxies!: ReadonlyMap<string, ProxyAccountFacts>
67
+
68
+ /** @internal */
69
+ constructor(init: GeeqLedgerInit) {
70
+ this.chainNumber = init.chainNumber
71
+ this.blockNumber = init.blockNumber
72
+ this.targetBlockNumber = init.blockNumber + 1
73
+ this.nodes = Object.freeze([...init.nodes])
74
+ this.metadataLimits = init.metadataLimits
75
+ this.minimumTransfer = init.minimumTransfer
76
+ this.geeqNorm = init.geeqNorm
77
+ this.readAt = init.readAt
78
+ this.nodeUrl = init.nodeUrl
79
+ Object.defineProperty(this, 'accounts', {
80
+ value: init.accounts,
81
+ enumerable: false,
82
+ writable: false,
83
+ })
84
+ Object.defineProperty(this, 'proxies', {
85
+ value: init.proxies,
86
+ enumerable: false,
87
+ writable: false,
88
+ })
89
+ }
90
+
91
+ /** The nodes that will actually accept a transaction. */
92
+ get processingNodes(): readonly GeeqLedgerNode[] {
93
+ return this.nodes.filter((node) => node.processing)
94
+ }
95
+
96
+ /**
97
+ * Find a net actor record.
98
+ *
99
+ * By `host` this matches the bare IP and nothing else — every first-party client does
100
+ * the same, because a record's `port` is the node's internal listen port while clients
101
+ * connect through a proxy on another one. Matching on both finds nothing.
102
+ */
103
+ findNode(match: { readonly host: string } | { readonly nodeNumber: number }): GeeqLedgerNode | undefined {
104
+ if ('nodeNumber' in match) {
105
+ return this.nodes.find((node) => node.nodeNumber === match.nodeNumber)
106
+ }
107
+ return this.nodes.find((node) => node.host === match.host)
108
+ }
109
+
110
+ /** Every net actor record at one host. More than one is possible, and ambiguous. */
111
+ findNodes(host: string): readonly GeeqLedgerNode[] {
112
+ return this.nodes.filter((node) => node.host === host)
113
+ }
114
+
115
+ /**
116
+ * The signing account's coin record, or `undefined` when this chain has none for it.
117
+ *
118
+ * Absence is the ordinary state of a freshly generated keypair: an account appears in
119
+ * the ledger only once it has been funded.
120
+ */
121
+ findCoinAccount(account: AccountRef): GeeqCoinAccount | undefined {
122
+ const accountId = resolveAccountId(account)
123
+ const facts = this.accounts.get(toBase64(accountId))
124
+ if (facts === undefined) return undefined
125
+ return Object.freeze({
126
+ accountId,
127
+ kind: facts.kind,
128
+ nonce: facts.nonce,
129
+ balance: facts.balance,
130
+ nfts: facts.nfts,
131
+ })
132
+ }
133
+
134
+ /** A proxy record, or `undefined` when this chain has none with that id. */
135
+ findProxyAccount(account: AccountRef): GeeqProxyAccount | undefined {
136
+ const accountId = resolveAccountId(account)
137
+ const facts = this.proxies.get(toBase64(accountId))
138
+ if (facts === undefined) return undefined
139
+ return Object.freeze({ accountId, ...facts })
140
+ }
141
+
142
+ /**
143
+ * Every NFT mint proxy an account administers — the ones it can mint through.
144
+ *
145
+ * Found by the admin each record names, so it needs nothing remembered from when they
146
+ * were created.
147
+ */
148
+ findProxyAccounts(match: { readonly admin: AccountRef }): readonly GeeqProxyAccount[] {
149
+ const admin = toBase64(resolveAccountId(match.admin))
150
+ const found: GeeqProxyAccount[] = []
151
+ for (const [key, facts] of this.proxies) {
152
+ if (facts.admin !== undefined && toBase64(facts.admin) === admin) {
153
+ found.push(Object.freeze({ accountId: fromBase64(key), ...facts }))
154
+ }
155
+ }
156
+ return found
157
+ }
158
+
159
+ /** How many coin accounts this ledger carries. Mostly useful for diagnostics. */
160
+ get coinAccountCount(): number {
161
+ return this.accounts.size
162
+ }
163
+
164
+ /** Summary only — the account table is far too large to serialise. */
165
+ toJSON(): Record<string, unknown> {
166
+ return {
167
+ chainNumber: this.chainNumber,
168
+ blockNumber: this.blockNumber,
169
+ targetBlockNumber: this.targetBlockNumber,
170
+ nodes: this.nodes,
171
+ metadataLimits: this.metadataLimits,
172
+ minimumTransfer: this.minimumTransfer?.toString(),
173
+ geeqNorm: this.geeqNorm?.toString(),
174
+ coinAccountCount: this.coinAccountCount,
175
+ proxyAccountCount: this.proxies.size,
176
+ readAt: this.readAt,
177
+ nodeUrl: this.nodeUrl,
178
+ }
179
+ }
180
+
181
+ toString(): string {
182
+ return `GeeqLedger(chain ${this.chainNumber}, block ${this.blockNumber}, ${this.nodes.length} nodes)`
183
+ }
184
+ }
@@ -0,0 +1,129 @@
1
+ import {
2
+ NetActorStatus as CapnpNetActorStatus,
3
+ NetActorType as CapnpNetActorType,
4
+ } from '../capnp/netActorRec.capnp'
5
+
6
+ /** What kind of actor a net actor record describes. */
7
+ export type GeeqNodeRole =
8
+ | 'anchor-node'
9
+ | 'anon-node'
10
+ | 'cert-node'
11
+ | 'cert-authority'
12
+ | 'chain-admin'
13
+ | 'relay'
14
+ | 'treasury-agent'
15
+ | 'unknown'
16
+
17
+ /** What that actor is currently doing. */
18
+ export type GeeqNodeStatus =
19
+ | 'chain-admin'
20
+ | 'hub'
21
+ | 'leaving'
22
+ | 'listening'
23
+ | 'node'
24
+ | 'ready'
25
+ | 'relay'
26
+ | 'suspended'
27
+ | 'unknown'
28
+
29
+ const ROLES: Readonly<Record<number, GeeqNodeRole>> = Object.freeze({
30
+ [CapnpNetActorType.NET_ACTOR_TYPE_ANCHOR_NODE]: 'anchor-node',
31
+ [CapnpNetActorType.NET_ACTOR_TYPE_ANON_NODE]: 'anon-node',
32
+ [CapnpNetActorType.NET_ACTOR_TYPE_CERT_NODE]: 'cert-node',
33
+ [CapnpNetActorType.NET_ACTOR_TYPE_CERT_AUTH]: 'cert-authority',
34
+ [CapnpNetActorType.NET_ACTOR_TYPE_CHAIN_ADMIN]: 'chain-admin',
35
+ [CapnpNetActorType.NET_ACTOR_TYPE_RELAY]: 'relay',
36
+ [CapnpNetActorType.NET_ACTOR_TYPE_TREASURY_AGENT]: 'treasury-agent',
37
+ })
38
+
39
+ const STATUSES: Readonly<Record<number, GeeqNodeStatus>> = Object.freeze({
40
+ [CapnpNetActorStatus.NET_ACTOR_STATUS_CHAIN_ADMIN]: 'chain-admin',
41
+ [CapnpNetActorStatus.NET_ACTOR_STATUS_HUB]: 'hub',
42
+ [CapnpNetActorStatus.NET_ACTOR_STATUS_LEAVING]: 'leaving',
43
+ [CapnpNetActorStatus.NET_ACTOR_STATUS_LISTENING]: 'listening',
44
+ [CapnpNetActorStatus.NET_ACTOR_STATUS_NODE]: 'node',
45
+ [CapnpNetActorStatus.NET_ACTOR_STATUS_READY]: 'ready',
46
+ [CapnpNetActorStatus.NET_ACTOR_STATUS_RELAY]: 'relay',
47
+ [CapnpNetActorStatus.NET_ACTOR_STATUS_SUSPENDED]: 'suspended',
48
+ })
49
+
50
+ /**
51
+ * Unknown values map to `'unknown'` rather than throwing.
52
+ *
53
+ * This deliberately differs from `fromWasmRecordType`, which throws: a bad record type
54
+ * means a corrupt address, but a new actor kind on a newer chain must not make an
55
+ * otherwise-valid ledger undecodable. An `'unknown'` actor is never `processing`, so
56
+ * the safe outcome is the default.
57
+ */
58
+ export function toNodeRole(value: number): GeeqNodeRole {
59
+ return ROLES[value] ?? 'unknown'
60
+ }
61
+
62
+ export function toNodeStatus(value: number): GeeqNodeStatus {
63
+ return STATUSES[value] ?? 'unknown'
64
+ }
65
+
66
+ /** Roles that run the transaction handler at all. */
67
+ const PROCESSING_ROLES: ReadonlySet<GeeqNodeRole> = new Set<GeeqNodeRole>([
68
+ 'anon-node',
69
+ 'anchor-node',
70
+ 'cert-node',
71
+ ])
72
+
73
+ /** Statuses in which such a role actually accepts transactions. */
74
+ const PROCESSING_STATUSES: ReadonlySet<GeeqNodeStatus> = new Set<GeeqNodeStatus>(['node', 'hub'])
75
+
76
+ /**
77
+ * Whether this actor will accept a transaction.
78
+ *
79
+ * Mirrors `isActive()` in the node itself
80
+ * (geeq_node_core/src/wrappers/network-actor-record/index.ts). Anything else answers
81
+ * HTTP 400 `'not processing'`, or — for a relay — registers no handler at all.
82
+ */
83
+ export function isProcessing(role: GeeqNodeRole, status: GeeqNodeStatus): boolean {
84
+ return PROCESSING_ROLES.has(role) && PROCESSING_STATUSES.has(status)
85
+ }
86
+
87
+ /** One net actor record, as plain values. */
88
+ export interface GeeqLedgerNode {
89
+ /** `nodeNumSelf` — what a transaction must name, checked with strict equality. */
90
+ readonly nodeNumber: number
91
+ /** The bare IP the ledger records. Nodes are matched on this and nothing else. */
92
+ readonly host: string
93
+ /**
94
+ * The port recorded against this node in the ledger.
95
+ *
96
+ * **Not the port you connect to.** Clients always reach a node on 2082, where TLS is
97
+ * terminated; every first-party client ignores this field and rebuilds the URL from
98
+ * its own configuration. Exposed for diagnostics, and used only to break a tie when
99
+ * one address carries several records.
100
+ */
101
+ readonly port: number
102
+ readonly role: GeeqNodeRole
103
+ readonly status: GeeqNodeStatus
104
+ /**
105
+ * True when the protocol permits this node to accept transactions.
106
+ *
107
+ * **Not a liveness signal.** Nothing in the protocol evicts a crashed node from the
108
+ * net actor list, and `blockNumLiveness` is rewritten for every actor on every commit
109
+ * rather than per node, so it carries no health information at all. A node that died
110
+ * an hour ago still reads as processing here. The only way to learn a node is up is
111
+ * to talk to it.
112
+ */
113
+ readonly processing: boolean
114
+ /**
115
+ * `blockNumRemove` — the block at which this node's turn in the active set ends.
116
+ *
117
+ * Reset 100–355 blocks ahead each time the node is promoted, so it is a rotation
118
+ * deadline rather than a departure flag.
119
+ */
120
+ readonly retiresAtBlock: number
121
+ /**
122
+ * True when this node is inside its own shutdown window and will refuse work.
123
+ *
124
+ * The node's submit handler answers HTTP 503 `'Block chain is stopped'` once its
125
+ * `blockNumRemove` is less than its current block plus two, so this is knowable from
126
+ * the ledger before a transaction is ever built.
127
+ */
128
+ readonly retiring: boolean
129
+ }
@@ -0,0 +1,49 @@
1
+ import { Message } from 'capnp-ts'
2
+ import { AppLayLedger as CapnpAppLayLedger } from '../capnp/appLayLedger.capnp'
3
+
4
+ import { fromBase64, toBase64 } from '../encoding/base64'
5
+ import { ledgerUnreadable } from '../errors'
6
+
7
+ /** What the application layer ledger says one NFT stands for. */
8
+ export interface NftRecordFacts {
9
+ readonly attestHash: Uint8Array
10
+ readonly metadata: readonly string[]
11
+ }
12
+
13
+ /**
14
+ * The NFT records in an application layer ledger, keyed by the NFT's base64 id.
15
+ *
16
+ * `source` is what a node's `result` holds for `type=all`: base64 packed Cap'n Proto, the
17
+ * same form as the validation layer ledger. Other application records are skipped. As
18
+ * with that ledger, the message is dropped once read.
19
+ */
20
+ export function decodeNftRecords(source: string): Map<string, NftRecordFacts> {
21
+ const bytes = fromBase64(source, 'The application layer ledger')
22
+ const records = new Map<string, NftRecordFacts>()
23
+ const decoder = new TextDecoder()
24
+ try {
25
+ const ledger = new Message(bytes, true).getRoot(CapnpAppLayLedger)
26
+ for (const record of ledger.getAppLayRecs().toArray()) {
27
+ // `counterparty` is the union's zero value, so the arm is checked before it is read.
28
+ const recType = record.getRecType()
29
+ if (!recType.isNft() || !recType.hasNft()) continue
30
+ const data = recType.getNft().getAttestationSubgroup().getData()
31
+ const metadata = data
32
+ .getMetadata()
33
+ .toArray()
34
+ .sort((a, b) => a.getPosition() - b.getPosition())
35
+ .map((entry) => decoder.decode(new Uint8Array(entry.getItem().toArrayBuffer())))
36
+ records.set(toBase64(new Uint8Array(record.getAcctNumApp().toArrayBuffer())), {
37
+ attestHash: new Uint8Array(data.getAttestHash().toArrayBuffer()),
38
+ metadata,
39
+ })
40
+ }
41
+ } catch (cause) {
42
+ throw ledgerUnreadable(
43
+ 'The application layer ledger could not be read. The node is probably running a ' +
44
+ 'schema version this SDK does not know.',
45
+ cause,
46
+ )
47
+ }
48
+ return records
49
+ }
@@ -0,0 +1,82 @@
1
+ import type { ParamAcctRec } from '../capnp/paramAcctRec.capnp'
2
+
3
+ import { uint64ToPennies } from '../amount'
4
+ import type { GeeqMetadataLimits } from '../types'
5
+
6
+ /**
7
+ * Read the metadata limits a chain publishes in its parameter account list.
8
+ *
9
+ * `metadataVarNum` caps the NUMBER of entries and `metadataVarBytes` caps the bytes in
10
+ * ONE entry; there is no aggregate cap. The node enforces both at submit time, so
11
+ * having them here is what lets a transaction be rejected before it is signed rather
12
+ * than after it is sent.
13
+ *
14
+ * Returns `undefined` when either parameter is absent. A chain that does not publish
15
+ * them is a schema difference, not a caller error, so the preflight is skipped and the
16
+ * node stays the authority.
17
+ *
18
+ * `metadataFixedNum` / `metadataFixedBytes` exist in the schema but are read nowhere in
19
+ * the current node, so they are ignored here too.
20
+ */
21
+ export function readMetadataLimits(
22
+ records: readonly ParamAcctRec[],
23
+ ): GeeqMetadataLimits | undefined {
24
+ let maxEntries: number | undefined
25
+ let maxBytesPerEntry: number | undefined
26
+
27
+ for (const record of records) {
28
+ const param = record.getParamType()
29
+ if (maxEntries === undefined && param.isMetadataVarNum() && param.hasMetadataVarNum()) {
30
+ maxEntries = param.getMetadataVarNum().getCurrentValue()
31
+ continue
32
+ }
33
+ if (
34
+ maxBytesPerEntry === undefined &&
35
+ param.isMetadataVarBytes() &&
36
+ param.hasMetadataVarBytes()
37
+ ) {
38
+ maxBytesPerEntry = param.getMetadataVarBytes().getCurrentValue()
39
+ }
40
+ }
41
+
42
+ if (maxEntries === undefined || maxBytesPerEntry === undefined) return undefined
43
+ return Object.freeze({ maxEntries, maxBytesPerEntry })
44
+ }
45
+
46
+ /**
47
+ * The smallest transfer this chain will accept, in pennies: `minTxAmt × geeqNorm + 1`.
48
+ *
49
+ * The node rejects anything below `minTxAmt × geeqNorm` (geeq_node_core, Transaction
50
+ * Amount Conformity); the Geeq wallet asks for one penny more, and so does this. Knowing
51
+ * it here turns a rejected transaction into a message before it is signed.
52
+ *
53
+ * Returns `undefined` when either parameter is missing, in which case nothing is checked
54
+ * and the node stays the authority.
55
+ */
56
+ export function readMinimumTransfer(
57
+ records: readonly ParamAcctRec[],
58
+ ): bigint | undefined {
59
+ const geeqNorm = readGeeqNorm(records)
60
+ for (const record of records) {
61
+ const param = record.getParamType()
62
+ if (param.isMinTxAmt() && param.hasMinTxAmt()) {
63
+ if (geeqNorm === undefined) return undefined
64
+ return uint64ToPennies(param.getMinTxAmt().getCurrentValue()) * geeqNorm + 1n
65
+ }
66
+ }
67
+ return undefined
68
+ }
69
+
70
+ /**
71
+ * The chain's `geeqNorm` parameter: the multiplier behind its minimum transfer and every
72
+ * fee. `undefined` when the chain does not publish it.
73
+ */
74
+ export function readGeeqNorm(records: readonly ParamAcctRec[]): bigint | undefined {
75
+ for (const record of records) {
76
+ const param = record.getParamType()
77
+ if (param.isGeeqNorm() && param.hasGeeqNorm()) {
78
+ return uint64ToPennies(param.getGeeqNorm().getCurrentValue())
79
+ }
80
+ }
81
+ return undefined
82
+ }
@@ -0,0 +1,51 @@
1
+ import type { ProxyAcctRec } from '../capnp/proxyAcctRec.capnp'
2
+
3
+ import { uint64ToPennies } from '../amount'
4
+
5
+ /** Which kind of proxy a record is. Only `'nft-mint'` is read further. */
6
+ export type GeeqProxyKind = 'nft-mint' | 'token-mint' | 'funding' | 'multisig' | 'dce' | 'unknown'
7
+
8
+ /** One proxy account record, as plain values. */
9
+ export interface GeeqProxyAccount {
10
+ readonly accountId: Uint8Array
11
+ readonly kind: GeeqProxyKind
12
+ /**
13
+ * The proxy's own nonce. A mint through it spends this one, not the admin's — a proxy
14
+ * carries its nonce on the record itself.
15
+ */
16
+ readonly nonce: number
17
+ /** Balance in pennies. Use `formatGeeq` to show it. */
18
+ readonly balance: bigint
19
+ /** The account id of the only key that can sign for it. Read for NFT mint proxies only. */
20
+ readonly admin: Uint8Array | undefined
21
+ }
22
+
23
+ /** What we keep per proxy. The id is the map key, so it is not repeated here. */
24
+ export type ProxyAccountFacts = Omit<GeeqProxyAccount, 'accountId'>
25
+
26
+ /**
27
+ * Read a proxy record's kind and admin.
28
+ *
29
+ * The same zero-value trap as a coin account: `DCE` is the union's zero value, so every
30
+ * arm is checked with `isX()` before anything is read from it.
31
+ */
32
+ export function readProxyAccount(record: ProxyAcctRec): ProxyAccountFacts {
33
+ const recType = record.getRecType()
34
+ const nonce = record.getNonce()
35
+ const balance = uint64ToPennies(record.getAmtCoin())
36
+
37
+ if (recType.isNftMint() && recType.hasNftMint()) {
38
+ const admin = new Uint8Array(recType.getNftMint().getAcctNumAdminUser().toArrayBuffer())
39
+ return { kind: 'nft-mint', nonce, balance, admin }
40
+ }
41
+ const kind = recType.isTokenMint()
42
+ ? 'token-mint'
43
+ : recType.isFunding()
44
+ ? 'funding'
45
+ : recType.isMultisig()
46
+ ? 'multisig'
47
+ : recType.isDce() && recType.hasDce()
48
+ ? 'dce'
49
+ : 'unknown'
50
+ return { kind, nonce, balance, admin: undefined }
51
+ }