@geeqdev/geeq-sdk 0.1.0 → 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,231 @@
1
+ import { invalidArgument, nodeUnusable } from '../errors'
2
+ import { GEEQ_NODE_PORT } from '../chain/select'
3
+ import { chainsRoute, nodesRoute } from './routes'
4
+ import type { RequestOptions } from './transport'
5
+ import { requestText } from './transport'
6
+
7
+ /** How the directory classifies a node. Only `'active'` processes transactions. */
8
+ export type GeeqNodeKind = 'active' | 'relay' | 'listener' | 'chain-admin' | 'none' | 'unknown'
9
+
10
+ /** One chain the directory knows about. */
11
+ export interface GeeqDiscoveredChain {
12
+ /** The directory's own row id, which every other directory route calls `systemId`. */
13
+ readonly systemId: number
14
+ /** The protocol chain number — what a transaction carries and a node checks. */
15
+ readonly chainNumber: number
16
+ readonly name: string | undefined
17
+ }
18
+
19
+ /**
20
+ * One node the directory knows about.
21
+ *
22
+ * Note what is **missing**: a node number. The directory can say where a node is but
23
+ * not who it is in the chain's own net actor list, so the ledger read is still the only
24
+ * way to learn the number a transaction must carry.
25
+ */
26
+ export interface GeeqDiscoveredNode {
27
+ readonly host: string
28
+ readonly kind: GeeqNodeKind
29
+ /** The directory's own lifecycle state — `new`, `ready`, `stopped`. */
30
+ readonly status: string
31
+ /** Where to reach it. Always the client port; the directory's own port is internal. */
32
+ readonly url: string
33
+ }
34
+
35
+ const KINDS: ReadonlySet<string> = new Set([
36
+ 'active',
37
+ 'relay',
38
+ 'listener',
39
+ 'chain-admin',
40
+ 'none',
41
+ ])
42
+
43
+ function resultOf(body: string, url: string): unknown[] {
44
+ let parsed: unknown
45
+ try {
46
+ parsed = JSON.parse(body)
47
+ } catch (cause) {
48
+ throw nodeUnusable(
49
+ `The directory at ${url} did not answer with JSON. A proxy or login page in front ` +
50
+ 'of it is the usual reason.',
51
+ cause,
52
+ )
53
+ }
54
+ const result = (parsed as { result?: unknown })?.result
55
+ if (!Array.isArray(result)) {
56
+ throw nodeUnusable(
57
+ `The directory at ${url} answered without a "result" array, so it does not look ` +
58
+ 'like a Geeq node directory.',
59
+ )
60
+ }
61
+ return result
62
+ }
63
+
64
+ /** Every chain the directory knows about. */
65
+ export async function listChains(
66
+ origin: string,
67
+ request: RequestOptions,
68
+ ): Promise<GeeqDiscoveredChain[]> {
69
+ const url = chainsRoute(origin)
70
+ const rows = resultOf(await requestText(url, request), url)
71
+
72
+ return rows
73
+ .map((row) => row as { id?: unknown; chainNumber?: unknown; name?: unknown })
74
+ .filter((row) => typeof row.id === 'number' && typeof row.chainNumber === 'number')
75
+ .map((row) =>
76
+ Object.freeze({
77
+ systemId: row.id as number,
78
+ chainNumber: row.chainNumber as number,
79
+ name: typeof row.name === 'string' ? row.name : undefined,
80
+ }),
81
+ )
82
+ }
83
+
84
+ export interface NodeAddressing {
85
+ /** Scheme for the node URLs. Defaults to the directory's own. */
86
+ readonly protocol?: string | undefined
87
+ /** Client port. Defaults to {@link GEEQ_NODE_PORT}; override only for local setups. */
88
+ readonly port?: number | undefined
89
+ }
90
+
91
+ /** Every node the directory lists for one chain. */
92
+ export async function listNodes(
93
+ origin: string,
94
+ systemId: number,
95
+ request: RequestOptions,
96
+ addressing: NodeAddressing = {},
97
+ ): Promise<GeeqDiscoveredNode[]> {
98
+ const protocol = addressing.protocol ?? new URL(origin).protocol
99
+ const port = addressing.port ?? GEEQ_NODE_PORT
100
+ const url = nodesRoute(origin, systemId)
101
+ const rows = resultOf(await requestText(url, request), url)
102
+
103
+ return rows
104
+ .map((row) => row as { ip?: unknown; type?: unknown; status?: unknown })
105
+ .filter((row) => typeof row.ip === 'string' && row.ip !== '')
106
+ .map((row) => {
107
+ const kind = typeof row.type === 'string' && KINDS.has(row.type) ? row.type : 'unknown'
108
+ return Object.freeze({
109
+ host: row.ip as string,
110
+ kind: kind as GeeqNodeKind,
111
+ status: typeof row.status === 'string' ? row.status : 'unknown',
112
+ // The directory reports the node's own internal listener, which is not where
113
+ // clients connect, so its port field is deliberately ignored in favour of the
114
+ // one port every node answers on.
115
+ url: `${protocol}//${row.ip as string}:${port}`,
116
+ })
117
+ })
118
+ }
119
+
120
+ export interface DiscoverOptions extends NodeAddressing {
121
+ readonly origin: string
122
+ /** Select the chain by the name the directory gives it. Matched case-insensitively. */
123
+ readonly chainName?: string | undefined
124
+ /** Select by protocol chain number, or — alongside `chainName` — assert it. */
125
+ readonly chainNumber?: number | undefined
126
+ readonly request: RequestOptions
127
+ }
128
+
129
+ export interface Discovery {
130
+ readonly chain: GeeqDiscoveredChain
131
+ /** Every node listed for the chain, in directory order. */
132
+ readonly nodes: readonly GeeqDiscoveredNode[]
133
+ /** Those that will actually take a transaction. */
134
+ readonly usable: readonly GeeqDiscoveredNode[]
135
+ }
136
+
137
+ /** True when the directory says this node is an active one that has not been stopped. */
138
+ export function isUsable(node: GeeqDiscoveredNode): boolean {
139
+ return node.kind === 'active' && node.status !== 'stopped'
140
+ }
141
+
142
+ /**
143
+ * Find a chain in the directory and list its nodes.
144
+ *
145
+ * This reads two routes and nothing else. The directory is a deployment control plane:
146
+ * it has no authentication, and its other routes create and destroy machines and serve
147
+ * private keys. Treat every value it returns as a hint about *where* to look, never as
148
+ * authority about the chain itself — that comes from the ledger.
149
+ */
150
+ export async function discover(options: DiscoverOptions): Promise<Discovery> {
151
+ const chains = await listChains(options.origin, options.request)
152
+
153
+ if (chains.length === 0) {
154
+ throw nodeUnusable(`The directory at ${options.origin} lists no chains.`)
155
+ }
156
+
157
+ const listed = chains
158
+ .map((c) => `${c.name ?? '(unnamed)'} (chain ${c.chainNumber})`)
159
+ .join(', ')
160
+
161
+ let chain: GeeqDiscoveredChain | undefined
162
+
163
+ if (options.chainName !== undefined) {
164
+ // Names are what a person types, so match them the way a person means them.
165
+ const wanted = options.chainName.trim().toLowerCase()
166
+ const matches = chains.filter((c) => c.name?.trim().toLowerCase() === wanted)
167
+
168
+ if (matches.length === 0) {
169
+ throw invalidArgument(
170
+ `The directory at ${options.origin} has no chain named ` +
171
+ `${JSON.stringify(options.chainName)}. Available: ${listed}.`,
172
+ )
173
+ }
174
+ if (matches.length > 1) {
175
+ throw invalidArgument(
176
+ `The directory at ${options.origin} lists ${matches.length} chains named ` +
177
+ `${JSON.stringify(options.chainName)} (numbers ` +
178
+ `${matches.map((c) => c.chainNumber).join(', ')}), so the name does not identify ` +
179
+ 'one. Select it by chainNumber instead.',
180
+ )
181
+ }
182
+ chain = matches[0]
183
+ } else if (options.chainNumber !== undefined) {
184
+ chain = chains.find((c) => c.chainNumber === options.chainNumber)
185
+ if (chain === undefined) {
186
+ throw invalidArgument(
187
+ `The directory at ${options.origin} does not list chain ${options.chainNumber}. ` +
188
+ `Available: ${listed}.`,
189
+ )
190
+ }
191
+ } else {
192
+ if (chains.length > 1) {
193
+ throw invalidArgument(
194
+ `The directory at ${options.origin} lists ${chains.length} chains, so chainName ` +
195
+ `or chainNumber is required to choose one. Available: ${listed}.`,
196
+ )
197
+ }
198
+ chain = chains[0]
199
+ }
200
+
201
+ const resolved = chain as GeeqDiscoveredChain
202
+
203
+ // A name and a number together are an assertion, not two ways of choosing.
204
+ if (
205
+ options.chainName !== undefined &&
206
+ options.chainNumber !== undefined &&
207
+ resolved.chainNumber !== options.chainNumber
208
+ ) {
209
+ throw invalidArgument(
210
+ `The directory at ${options.origin} says ${JSON.stringify(options.chainName)} is ` +
211
+ `chain ${resolved.chainNumber}, but chainNumber was given as ` +
212
+ `${options.chainNumber}. Drop one of the two.`,
213
+ )
214
+ }
215
+ const nodes = await listNodes(options.origin, resolved.systemId, options.request, {
216
+ ...(options.protocol === undefined ? {} : { protocol: options.protocol }),
217
+ ...(options.port === undefined ? {} : { port: options.port }),
218
+ })
219
+ const usable = nodes.filter(isUsable)
220
+
221
+ if (usable.length === 0) {
222
+ const seen = nodes.map((n) => `${n.host} (${n.kind}/${n.status})`).join(', ')
223
+ throw nodeUnusable(
224
+ `The directory at ${options.origin} lists no node for chain ${resolved.chainNumber} ` +
225
+ 'that is active and running. Only an active node processes transactions. ' +
226
+ (seen === '' ? 'It listed none at all.' : `It listed: ${seen}.`),
227
+ )
228
+ }
229
+
230
+ return Object.freeze({ chain: resolved, nodes: Object.freeze([...nodes]), usable: Object.freeze(usable) })
231
+ }
@@ -0,0 +1,123 @@
1
+ import { GeeqError, invalidArgument, nodeUnusable } from '../errors'
2
+ import { fromBase64 } from '../encoding/base64'
3
+ import { toHex } from '../encoding/hex'
4
+ import { explorerTransactionRoute } from './routes'
5
+ import { defaultTransport, requestText } from './transport'
6
+ import type { GeeqTransport } from './transport'
7
+
8
+ const DEFAULT_TIMEOUT_MS = 10_000
9
+ const HASH_BYTES = 32
10
+ /** What the explorer answers, with HTTP 500, for a refHash it does not have. */
11
+ const NOT_FOUND = 'Transaction does not exist'
12
+
13
+ /** A transaction as a block explorer reports it, field for field. */
14
+ export interface GeeqExplorerTransaction {
15
+ /** Hex — the form the explorer files transactions under. */
16
+ readonly refHash: string
17
+ /** The transaction's kind, such as `coin`. */
18
+ readonly type: string
19
+ /** The explorer's verdict on it. */
20
+ readonly status: string
21
+ readonly block: number
22
+ readonly chainName: string
23
+ readonly chainNumber: number
24
+ /** The sender's wallet address. */
25
+ readonly from: string
26
+ readonly senderType: string
27
+ /** The receiver's wallet address, or empty when it has none. */
28
+ readonly to: string
29
+ readonly receiverType: string
30
+ readonly publicKey: string
31
+ readonly signature: string
32
+ readonly createdAt: string
33
+ readonly fee: string
34
+ readonly acctNumApp?: string
35
+ /** What it moved — coin, tokens, NFTs — with amounts as decimal strings. */
36
+ readonly values: readonly {
37
+ readonly type: string
38
+ readonly value: string
39
+ readonly name?: string
40
+ }[]
41
+ /** An attestation's hash and metadata. */
42
+ readonly payload?: {
43
+ readonly attestationHash: string
44
+ readonly metadata: readonly string[]
45
+ }
46
+ }
47
+
48
+ export interface ExplorerTransactionRequest {
49
+ /** The explorer's base URL, such as `https://blkexp.geeq-test.io`. */
50
+ readonly explorerUrl: string | URL
51
+ /**
52
+ * The transaction's refHash in base64 — what `transactionIdOf(tx)` returns and socket
53
+ * status events carry. It is sent to the explorer as hex.
54
+ */
55
+ readonly refHash: string
56
+ /** Defaults to `globalThis.fetch`. */
57
+ readonly transport?: GeeqTransport
58
+ /** Defaults to 10000. */
59
+ readonly timeoutMs?: number
60
+ readonly signal?: AbortSignal
61
+ }
62
+
63
+ function explorerBase(explorerUrl: string | URL): string {
64
+ let url: URL | undefined
65
+ try {
66
+ url = new URL(String(explorerUrl))
67
+ } catch {
68
+ url = undefined
69
+ }
70
+ if (url === undefined || (url.protocol !== 'https:' && url.protocol !== 'http:')) {
71
+ throw invalidArgument(
72
+ 'explorerUrl must be an absolute http(s) URL, such as https://blkexp.geeq-test.io, ' +
73
+ `received ${JSON.stringify(String(explorerUrl))}.`,
74
+ )
75
+ }
76
+ return `${url.origin}${url.pathname.replace(/\/+$/u, '')}`
77
+ }
78
+
79
+ /**
80
+ * Look a transaction up in a block explorer by its refHash.
81
+ *
82
+ * Resolves `null` when the explorer does not have it — not indexed yet, or never put in a
83
+ * block. The explorer only knows a transaction once a block has decided it, so for one
84
+ * you have just sent, the node socket answers first.
85
+ */
86
+ export async function getExplorerTransaction(
87
+ request: ExplorerTransactionRequest,
88
+ ): Promise<GeeqExplorerTransaction | null> {
89
+ const hash = fromBase64(request.refHash, 'refHash')
90
+ if (hash.length !== HASH_BYTES) {
91
+ throw invalidArgument(
92
+ `refHash must be the base64 of a ${HASH_BYTES}-byte hash, received ${hash.length} bytes.`,
93
+ )
94
+ }
95
+ const url = explorerTransactionRoute(explorerBase(request.explorerUrl), toHex(hash))
96
+
97
+ let body: string
98
+ try {
99
+ body = await requestText(url, {
100
+ transport: request.transport ?? defaultTransport,
101
+ timeoutMs: request.timeoutMs ?? DEFAULT_TIMEOUT_MS,
102
+ retries: 1,
103
+ signal: request.signal,
104
+ })
105
+ } catch (error) {
106
+ if (error instanceof GeeqError && error.nodeDetails === NOT_FOUND) return null
107
+ throw error
108
+ }
109
+
110
+ let result: unknown
111
+ try {
112
+ result = (JSON.parse(body) as { result?: unknown })?.result
113
+ } catch {
114
+ result = undefined
115
+ }
116
+ if (result === null || typeof result !== 'object' || !('refHash' in result)) {
117
+ throw nodeUnusable(
118
+ `${url} did not answer with a transaction, so it does not look like a Geeq block ` +
119
+ 'explorer.',
120
+ )
121
+ }
122
+ return result as GeeqExplorerTransaction
123
+ }
@@ -0,0 +1,58 @@
1
+ import { ledgerUnreadable } from '../errors'
2
+ import { decodeValidationLedger } from '../ledger/decode'
3
+ import type { GeeqLedger } from '../ledger/geeq-ledger'
4
+ import { decodeNftRecords } from '../ledger/nft-records'
5
+ import type { NftRecordFacts } from '../ledger/nft-records'
6
+ import { appLedgerRoute, ledgerRoute } from './routes'
7
+ import type { RequestOptions } from './transport'
8
+ import { requestText } from './transport'
9
+
10
+ /** Pull `result` out of a node's response envelope. */
11
+ function resultOf(body: string, url: string): string {
12
+ let parsed: unknown
13
+ try {
14
+ parsed = JSON.parse(body)
15
+ } catch (cause) {
16
+ throw ledgerUnreadable(
17
+ `The node at ${url} did not answer with JSON. A login page or a proxy error is ` +
18
+ `the usual reason. First 120 characters: ${JSON.stringify(body.slice(0, 120))}.`,
19
+ cause,
20
+ )
21
+ }
22
+
23
+ if (parsed === null || typeof parsed !== 'object' || !('result' in parsed)) {
24
+ throw ledgerUnreadable(
25
+ `The node at ${url} answered JSON with no "result" field, so this is probably ` +
26
+ 'not a Geeq node.',
27
+ )
28
+ }
29
+
30
+ const { result } = parsed as { result: unknown }
31
+ if (typeof result !== 'string') {
32
+ throw ledgerUnreadable(
33
+ `The node at ${url} answered with a "result" of type ${typeof result}, expected a ` +
34
+ 'base64 string.',
35
+ )
36
+ }
37
+ return result
38
+ }
39
+
40
+ /** Read and decode the validation layer ledger from one node. */
41
+ export async function readLedger(
42
+ origin: string,
43
+ options: RequestOptions,
44
+ ): Promise<GeeqLedger> {
45
+ const url = ledgerRoute(origin)
46
+ const body = await requestText(url, options)
47
+ return decodeValidationLedger(resultOf(body, url), { nodeUrl: origin })
48
+ }
49
+
50
+ /** Read the NFT records in the application layer ledger from one node. */
51
+ export async function readNftRecords(
52
+ origin: string,
53
+ options: RequestOptions,
54
+ ): Promise<Map<string, NftRecordFacts>> {
55
+ const url = appLedgerRoute(origin)
56
+ const body = await requestText(url, options)
57
+ return decodeNftRecords(resultOf(body, url))
58
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Every node route the SDK knows, in one place.
3
+ *
4
+ * `origin` is an absolute node URL with no trailing slash — see `originOf`.
5
+ */
6
+
7
+ /** `GET` the current validation layer ledger, base64 packed capnp under `result`. */
8
+ export function ledgerRoute(origin: string): string {
9
+ return `${origin}/api/blocks?type=vll`
10
+ }
11
+
12
+ /** `GET` the current application layer ledger, where NFT records live. Same envelope. */
13
+ export function appLedgerRoute(origin: string): string {
14
+ return `${origin}/api/blocks?type=all`
15
+ }
16
+
17
+ /**
18
+ * Where a signed transaction is POSTed.
19
+ *
20
+ * The SDK does not submit — this is exported so an application builds the same URL the
21
+ * SDK read from, rather than keeping its own copy of the path.
22
+ */
23
+ export function submitRoute(origin: string): string {
24
+ return `${origin}/api/blocks/process`
25
+ }
26
+
27
+ /**
28
+ * The node's client WebSocket, where transaction status events arrive.
29
+ *
30
+ * Same host and port as the HTTP API — the node's TLS front door routes `/ws/` to the
31
+ * socket server and `/api/` to the HTTP one — so the only change is the scheme. The
32
+ * trailing slash is part of the route: the proxy answers `/ws` with a redirect, and a
33
+ * WebSocket handshake treats a redirect as failure.
34
+ */
35
+ export function socketRoute(origin: string): string {
36
+ const url = new URL(origin)
37
+ const scheme =
38
+ url.protocol === 'https:' ? 'wss:' : url.protocol === 'http:' ? 'ws:' : url.protocol
39
+ return `${scheme}//${url.host}${url.pathname.replace(/\/+$/u, '')}/ws/`
40
+ }
41
+
42
+ /**
43
+ * The body a node expects for an application-layer transaction: an attestation or an NFT
44
+ * mint, each a transaction and its payload.
45
+ */
46
+ export function attestationSubmitBody(transaction: string, payload: string): string {
47
+ return JSON.stringify({ type: 'uat', data: { transaction, payload } })
48
+ }
49
+
50
+ /**
51
+ * The body a node expects for a coin transfer.
52
+ *
53
+ * A different type and a different shape from an attestation: a validation-layer
54
+ * transaction carries no separate payload, so there is one blob rather than two.
55
+ */
56
+ export function coinTransferSubmitBody(transaction: string): string {
57
+ return JSON.stringify({ type: 'uvt', data: { transaction } })
58
+ }
59
+
60
+ /**
61
+ * Discovery routes on the chain's directory service.
62
+ *
63
+ * Only these two are ever called. The same service also exposes routes that start and
64
+ * stop nodes, install a genesis block, and — on `/api/accounts` — hand out plaintext
65
+ * private keys. None of those belong anywhere near an SDK, so the paths live here and
66
+ * nowhere else, and nothing constructs a URL against that origin by hand.
67
+ */
68
+ export function chainsRoute(origin: string): string {
69
+ return `${origin}/api/admin/simulation/chains`
70
+ }
71
+
72
+ export function nodesRoute(origin: string, systemId: number): string {
73
+ return `${origin}/api/nodes?systemId=${encodeURIComponent(String(systemId))}`
74
+ }
75
+
76
+ /** A block explorer's transaction lookup. The explorer files transactions by hex refHash. */
77
+ export function explorerTransactionRoute(explorer: string, refHashHex: string): string {
78
+ return `${explorer}/api/transactions/byRefHash?refHash=${refHashHex}`
79
+ }