@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.
- package/.idea/geeq_sdk.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/.nvmrc +1 -0
- package/CONTRIBUTING.md +20 -0
- package/example/.env.example +4 -0
- package/example/README.md +304 -0
- package/example/USAGE.md +74 -0
- package/example/index.html +12 -0
- package/example/package-lock.json +1557 -0
- package/example/package.json +25 -0
- package/example/scripts/browser-check.mjs +1421 -0
- package/example/scripts/match-derivation.mjs +65 -0
- package/example/src/App.vue +138 -0
- package/example/src/components/AuthorityPanel.vue +277 -0
- package/example/src/components/ChainPanel.vue +267 -0
- package/example/src/components/ChainState.vue +59 -0
- package/example/src/components/CopyButton.vue +25 -0
- package/example/src/components/CreateAssetPanel.vue +313 -0
- package/example/src/components/DigitalAssetsPanel.vue +68 -0
- package/example/src/components/EvidencePanel.vue +396 -0
- package/example/src/components/ExplorerPanel.vue +119 -0
- package/example/src/components/KeypairPanel.vue +182 -0
- package/example/src/components/PhrasePanel.vue +105 -0
- package/example/src/components/SocketWarning.vue +18 -0
- package/example/src/components/StepGate.vue +24 -0
- package/example/src/components/TransferPanel.vue +416 -0
- package/example/src/components/TxDeadline.vue +19 -0
- package/example/src/components/TxFee.vue +31 -0
- package/example/src/components/TxStatus.vue +105 -0
- package/example/src/composables/useSocketConnection.ts +80 -0
- package/example/src/env.d.ts +12 -0
- package/example/src/main.ts +5 -0
- package/example/src/pennies.ts +7 -0
- package/example/src/style.css +149 -0
- package/example/tsconfig.json +19 -0
- package/example/vite.config.ts +26 -0
- package/jest.config.js +18 -0
- package/package.json +1 -5
- package/scripts/copy-wasm.mjs +21 -0
- package/scripts/smoke.mjs +382 -0
- package/src/address/record-type.ts +77 -0
- package/src/address/wallet-address.ts +105 -0
- package/src/amount.ts +143 -0
- package/src/assert.ts +30 -0
- package/src/capnp/README.md +15 -0
- package/src/capnp/adminNetTx.capnp.ts +66 -0
- package/src/capnp/appLayBlock.capnp.ts +26 -0
- package/src/capnp/appLayLedger.capnp.ts +35 -0
- package/src/capnp/appLayRec.capnp.ts +159 -0
- package/src/capnp/chainStateView.capnp.ts +49 -0
- package/src/capnp/coinAcctRec.capnp.ts +193 -0
- package/src/capnp/genesisBlock.capnp.ts +56 -0
- package/src/capnp/hubContentStmt.capnp.ts +59 -0
- package/src/capnp/misc.capnp.ts +19 -0
- package/src/capnp/netActorRec.capnp.ts +64 -0
- package/src/capnp/nodeContentStmt.capnp.ts +62 -0
- package/src/capnp/nodePayloadList.capnp.ts +28 -0
- package/src/capnp/nodeTxList.capnp.ts +37 -0
- package/src/capnp/paramAcctRec.capnp.ts +669 -0
- package/src/capnp/payload.capnp.ts +261 -0
- package/src/capnp/proxyAcctRec.capnp.ts +259 -0
- package/src/capnp/relayContentStmt.capnp.ts +85 -0
- package/src/capnp/sysAcctRec.capnp.ts +47 -0
- package/src/capnp/userAppTx.capnp.ts +269 -0
- package/src/capnp/userValTx.capnp.ts +1999 -0
- package/src/capnp/valLayBlock.capnp.ts +93 -0
- package/src/capnp/valLayLedger.capnp.ts +80 -0
- package/src/chain/geeq-chain.ts +1094 -0
- package/src/chain/geeq-node.ts +105 -0
- package/src/chain/nonce-guard.ts +73 -0
- package/src/chain/select.ts +149 -0
- package/src/crypto/account-id.ts +83 -0
- package/src/crypto/keypair.ts +221 -0
- package/src/crypto/mnemonic.ts +69 -0
- package/src/encoding/base64.ts +58 -0
- package/src/encoding/hex.ts +42 -0
- package/src/errors.ts +122 -0
- package/src/index.ts +195 -0
- package/src/ledger/coin-accounts.ts +73 -0
- package/src/ledger/decode.ts +147 -0
- package/src/ledger/geeq-ledger.ts +184 -0
- package/src/ledger/net-actors.ts +129 -0
- package/src/ledger/nft-records.ts +49 -0
- package/src/ledger/params.ts +82 -0
- package/src/ledger/proxy-accounts.ts +51 -0
- package/src/net/discovery.ts +231 -0
- package/src/net/explorer.ts +123 -0
- package/src/net/read-ledger.ts +58 -0
- package/src/net/routes.ts +79 -0
- package/src/net/transport.ts +221 -0
- package/src/net/websocket.ts +94 -0
- package/src/runtime/bindings.ts +38 -0
- package/src/runtime/init.ts +49 -0
- package/src/runtime/wasm-loader.browser.ts +22 -0
- package/src/runtime/wasm-loader.node.ts +16 -0
- package/src/socket/geeq-socket.ts +827 -0
- package/src/socket/tracked-tx.ts +319 -0
- package/src/socket/tx-status.ts +131 -0
- package/src/tx/attestation.ts +222 -0
- package/src/tx/content.ts +82 -0
- package/src/tx/encode.ts +107 -0
- package/src/tx/expiry.ts +11 -0
- package/src/tx/fee.ts +31 -0
- package/src/tx/metadata.ts +100 -0
- package/src/tx/nft.ts +108 -0
- package/src/tx/proxy.ts +156 -0
- package/src/tx/transfer.ts +162 -0
- package/src/types.ts +503 -0
- package/src/wasm/README.md +22 -0
- package/src/wasm/browser/geeq_core.d.ts +1224 -0
- package/src/wasm/browser/geeq_core.js +6014 -0
- package/src/wasm/browser/geeq_core_bg.wasm +0 -0
- package/src/wasm/browser/geeq_core_bg.wasm.d.ts +447 -0
- package/src/wasm/nodejs/geeq_core.d.ts +753 -0
- package/src/wasm/nodejs/geeq_core.js +6016 -0
- package/src/wasm/nodejs/geeq_core_bg.wasm +0 -0
- package/src/wasm/nodejs/geeq_core_bg.wasm.d.ts +447 -0
- package/src/wasm/nodejs/package.json +3 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +60 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { GeeqError, networkUnreachable, nodeError } from '../errors'
|
|
2
|
+
|
|
3
|
+
/** A response, narrowed to what this SDK reads. A WHATWG `Response` satisfies it. */
|
|
4
|
+
export interface GeeqHttpResponse {
|
|
5
|
+
readonly ok: boolean
|
|
6
|
+
readonly status: number
|
|
7
|
+
text(): Promise<string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A request, narrowed to what this SDK sends. Assignable to `RequestInit`. */
|
|
11
|
+
export interface GeeqRequestInit {
|
|
12
|
+
readonly method?: string
|
|
13
|
+
readonly headers?: Readonly<Record<string, string>>
|
|
14
|
+
readonly body?: string
|
|
15
|
+
readonly signal?: AbortSignal
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* How the SDK reaches a node.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately a structural subset of WHATWG `fetch`, so `{ transport: fetch }`
|
|
22
|
+
* typechecks with no adapter, and so does undici with a custom dispatcher, a relative
|
|
23
|
+
* URL through a dev proxy, or a scripted array of responses in a test.
|
|
24
|
+
*/
|
|
25
|
+
export type GeeqTransport = (
|
|
26
|
+
url: string,
|
|
27
|
+
init?: GeeqRequestInit,
|
|
28
|
+
) => Promise<GeeqHttpResponse>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `globalThis.fetch`, resolved at call time.
|
|
32
|
+
*
|
|
33
|
+
* Never a stored reference: an unbound `fetch` throws `Illegal invocation` in a
|
|
34
|
+
* browser, and capturing it would defeat any test that swaps `globalThis.fetch` after
|
|
35
|
+
* this module is imported.
|
|
36
|
+
*/
|
|
37
|
+
export const defaultTransport: GeeqTransport = (url, init) =>
|
|
38
|
+
globalThis.fetch(url, init as RequestInit)
|
|
39
|
+
|
|
40
|
+
export interface RequestOptions {
|
|
41
|
+
readonly transport: GeeqTransport
|
|
42
|
+
readonly timeoutMs: number
|
|
43
|
+
readonly retries: number
|
|
44
|
+
readonly headers?: Readonly<Record<string, string>> | undefined
|
|
45
|
+
readonly signal?: AbortSignal | undefined
|
|
46
|
+
readonly method?: string | undefined
|
|
47
|
+
readonly body?: string | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The one failure a browser can be certain about before making a request.
|
|
52
|
+
*
|
|
53
|
+
* An https page may not load an http subresource. The browser blocks it and reports a
|
|
54
|
+
* generic network failure, so issuing the request would only turn a knowable cause into
|
|
55
|
+
* an ambiguous one.
|
|
56
|
+
*/
|
|
57
|
+
function assertSchemeReachable(url: string): void {
|
|
58
|
+
const location = (globalThis as { location?: { protocol?: string } }).location
|
|
59
|
+
if (location?.protocol !== 'https:') return
|
|
60
|
+
|
|
61
|
+
let scheme: string
|
|
62
|
+
try {
|
|
63
|
+
scheme = new URL(url).protocol
|
|
64
|
+
} catch {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (scheme !== 'http:') return
|
|
68
|
+
|
|
69
|
+
throw networkUnreachable(
|
|
70
|
+
`This page is served over https and the node URL ${url} is http, so the browser ` +
|
|
71
|
+
'will block the request as mixed content before it is sent. Use the node\'s https ' +
|
|
72
|
+
'URL (port 2082 by default), or serve this page over http while developing.',
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The four causes a browser cannot tell apart, each with what to do about it. */
|
|
77
|
+
function opaqueFailureMessage(url: string): string {
|
|
78
|
+
return (
|
|
79
|
+
`Could not reach the Geeq node at ${url}. A browser reports network failures ` +
|
|
80
|
+
'opaquely, so this is one of four things: (1) the node does not answer CORS for ' +
|
|
81
|
+
'this origin — pass your own `transport` routing through a proxy you control; ' +
|
|
82
|
+
"(2) the node's TLS certificate is not trusted, which is common because nodes are " +
|
|
83
|
+
'addressed by bare IP — open the URL in a tab to see the interstitial; (3) this ' +
|
|
84
|
+
'page is https and the node is http, which browsers block; or (4) the node is down ' +
|
|
85
|
+
'or the URL is wrong.'
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function describeCause(cause: unknown): string | undefined {
|
|
90
|
+
if (cause !== null && typeof cause === 'object') {
|
|
91
|
+
const { code } = cause as { code?: unknown }
|
|
92
|
+
if (typeof code === 'string') return code
|
|
93
|
+
}
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Pull the node's own `details` out of an error body without trusting its shape. */
|
|
98
|
+
function nodeDetailsOf(body: string): string | undefined {
|
|
99
|
+
try {
|
|
100
|
+
const parsed: unknown = JSON.parse(body)
|
|
101
|
+
if (parsed !== null && typeof parsed === 'object') {
|
|
102
|
+
const { details, error } = parsed as { details?: unknown; error?: unknown }
|
|
103
|
+
if (typeof details === 'string' && details !== '') return details
|
|
104
|
+
// A block explorer sends its reasons as a list.
|
|
105
|
+
if (Array.isArray(details)) {
|
|
106
|
+
const joined = details.filter((item) => typeof item === 'string').join('; ')
|
|
107
|
+
if (joined !== '') return joined
|
|
108
|
+
}
|
|
109
|
+
if (typeof error === 'string' && error !== '') return error
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Not JSON. The raw body is still the most useful thing we have.
|
|
113
|
+
}
|
|
114
|
+
return body === '' ? undefined : body.slice(0, 400)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* `signal.throwIfAborted()`, written out: a polyfilled `AbortSignal` may not have it.
|
|
119
|
+
* A call rather than an inline check also keeps TypeScript from narrowing `aborted` to
|
|
120
|
+
* false for the rest of the function — it can change across an `await`.
|
|
121
|
+
*/
|
|
122
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
123
|
+
if (signal?.aborted === true) throw signal.reason
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Make one request and return its body, mapping every failure onto a typed error.
|
|
128
|
+
*
|
|
129
|
+
* Retries only a transport failure, and only because reads are idempotent. A response
|
|
130
|
+
* that arrived is never retried: the node has decided.
|
|
131
|
+
*/
|
|
132
|
+
export async function requestText(url: string, options: RequestOptions): Promise<string> {
|
|
133
|
+
assertSchemeReachable(url)
|
|
134
|
+
|
|
135
|
+
const { signal } = options
|
|
136
|
+
let lastCause: unknown
|
|
137
|
+
for (let attempt = 0; attempt <= options.retries; attempt += 1) {
|
|
138
|
+
// An 'abort' listener never fires for a signal that is already aborted, so without
|
|
139
|
+
// this the request would go out anyway — and so would a retry after an abort.
|
|
140
|
+
throwIfAborted(signal)
|
|
141
|
+
|
|
142
|
+
// Composed by hand rather than with AbortSignal.any, which needs Node 20.3 and
|
|
143
|
+
// Safari 17.4 — too new for a library that supports Node 20. The caller's reason is
|
|
144
|
+
// passed on, so a fetch that honours it rejects with exactly what the caller aborted with.
|
|
145
|
+
const controller = new AbortController()
|
|
146
|
+
const onAbort = (): void => controller.abort(signal?.reason)
|
|
147
|
+
signal?.addEventListener('abort', onAbort)
|
|
148
|
+
let timedOut = false
|
|
149
|
+
const timer = setTimeout(() => {
|
|
150
|
+
timedOut = true
|
|
151
|
+
controller.abort()
|
|
152
|
+
}, options.timeoutMs)
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const response = await options.transport(url, {
|
|
156
|
+
signal: controller.signal,
|
|
157
|
+
...(options.method === undefined ? {} : { method: options.method }),
|
|
158
|
+
...(options.headers === undefined ? {} : { headers: options.headers }),
|
|
159
|
+
...(options.body === undefined ? {} : { body: options.body }),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const body = await response.text()
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
const details = nodeDetailsOf(body)
|
|
165
|
+
throw nodeError(
|
|
166
|
+
`The node at ${url} answered HTTP ${response.status}` +
|
|
167
|
+
(details === undefined ? ' with no detail.' : `: ${JSON.stringify(details)}.`),
|
|
168
|
+
response.status,
|
|
169
|
+
details,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
return body
|
|
173
|
+
} catch (cause) {
|
|
174
|
+
if (cause instanceof GeeqError) throw cause
|
|
175
|
+
|
|
176
|
+
// A caller-driven abort is theirs to observe, not ours to relabel.
|
|
177
|
+
if (signal?.aborted === true) throw signal.reason
|
|
178
|
+
|
|
179
|
+
if (timedOut) {
|
|
180
|
+
throw networkUnreachable(
|
|
181
|
+
`The node at ${url} did not answer within ${options.timeoutMs}ms. Raise ` +
|
|
182
|
+
'timeoutMs if the node is simply slow, or try another node.',
|
|
183
|
+
cause,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
lastCause = cause
|
|
187
|
+
} finally {
|
|
188
|
+
clearTimeout(timer)
|
|
189
|
+
signal?.removeEventListener('abort', onAbort)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const code = describeCause(lastCause)
|
|
194
|
+
throw networkUnreachable(
|
|
195
|
+
code === undefined
|
|
196
|
+
? opaqueFailureMessage(url)
|
|
197
|
+
: `Could not reach the Geeq node at ${url}: ${code}.`,
|
|
198
|
+
lastCause,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Wait for `promise`, or stop waiting when `signal` aborts.
|
|
204
|
+
*
|
|
205
|
+
* For a request several callers share: aborting one caller's wait must not cancel the
|
|
206
|
+
* request out from under the others, so the request runs on without any caller's signal
|
|
207
|
+
* and each caller races its own against it.
|
|
208
|
+
*
|
|
209
|
+
* @internal
|
|
210
|
+
*/
|
|
211
|
+
export function raceAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
|
212
|
+
if (signal === undefined) return promise
|
|
213
|
+
if (signal.aborted) return Promise.reject(signal.reason)
|
|
214
|
+
return new Promise<T>((resolve, reject) => {
|
|
215
|
+
const onAbort = (): void => reject(signal.reason)
|
|
216
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
217
|
+
promise
|
|
218
|
+
.then(resolve, reject)
|
|
219
|
+
.finally(() => signal.removeEventListener('abort', onAbort))
|
|
220
|
+
})
|
|
221
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { socketUnreachable } from '../errors'
|
|
2
|
+
|
|
3
|
+
/** `readyState` of an open connection. The same number in browsers and in `ws`. */
|
|
4
|
+
export const WEBSOCKET_OPEN = 1
|
|
5
|
+
|
|
6
|
+
/** What a WebSocket hands its `message` listeners, narrowed to the one field read. */
|
|
7
|
+
export interface GeeqWebSocketMessageEvent {
|
|
8
|
+
readonly data: unknown
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A WebSocket, narrowed to what this SDK uses.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately structural, like {@link GeeqTransport}: a browser `WebSocket`, Node 22's
|
|
15
|
+
* global one and the `ws` package's class all satisfy it with no adapter, and so does a
|
|
16
|
+
* scripted fake in a test. Listeners are attached with `addEventListener` rather than
|
|
17
|
+
* `onmessage` and friends because a method is compared bivariantly, which is what lets
|
|
18
|
+
* each implementation's own event types through.
|
|
19
|
+
*/
|
|
20
|
+
export interface GeeqWebSocketLike {
|
|
21
|
+
readonly readyState: number
|
|
22
|
+
send(data: string): void
|
|
23
|
+
close(code?: number, reason?: string): void
|
|
24
|
+
addEventListener(type: 'open', listener: (event: unknown) => void): void
|
|
25
|
+
addEventListener(type: 'message', listener: (event: GeeqWebSocketMessageEvent) => void): void
|
|
26
|
+
addEventListener(type: 'close', listener: (event: unknown) => void): void
|
|
27
|
+
addEventListener(type: 'error', listener: (event: unknown) => void): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How the SDK opens a socket.
|
|
32
|
+
*
|
|
33
|
+
* `(url) => new WebSocket(url)` is the whole contract. Pass one to use the `ws` package on
|
|
34
|
+
* Node 20, to add headers or a TLS agent there, or to script a connection in a test.
|
|
35
|
+
*/
|
|
36
|
+
export type GeeqWebSocketFactory = (url: string) => GeeqWebSocketLike
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `new globalThis.WebSocket(url)`, resolved at call time.
|
|
40
|
+
*
|
|
41
|
+
* Never a stored reference, for the same reason as `defaultTransport`: a test that swaps
|
|
42
|
+
* the global after import must still be honoured. Node 20 has no global WebSocket at all,
|
|
43
|
+
* and a library that supports Node 20 cannot assume one, so its absence is reported with
|
|
44
|
+
* the one-line fix rather than as a `TypeError`.
|
|
45
|
+
*/
|
|
46
|
+
export const defaultWebSocketFactory: GeeqWebSocketFactory = (url) => {
|
|
47
|
+
const WebSocketClass = (globalThis as { WebSocket?: new (url: string) => GeeqWebSocketLike })
|
|
48
|
+
.WebSocket
|
|
49
|
+
if (typeof WebSocketClass !== 'function') {
|
|
50
|
+
throw socketUnreachable(
|
|
51
|
+
'This runtime has no global WebSocket — Node 20 does not, Node 22 and every browser ' +
|
|
52
|
+
'do. Pass one explicitly: `webSocket: (url) => new WebSocket(url)` with ' +
|
|
53
|
+
'`import WebSocket from "ws"`.',
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
return new WebSocketClass(url)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The one socket failure a browser can be certain about before trying.
|
|
61
|
+
*
|
|
62
|
+
* An https page may not open a `ws://` socket; the browser refuses it outright and reports
|
|
63
|
+
* nothing useful, so the handshake would only turn a knowable cause into an opaque one.
|
|
64
|
+
*/
|
|
65
|
+
export function assertSocketSchemeReachable(url: string): void {
|
|
66
|
+
const location = (globalThis as { location?: { protocol?: string } }).location
|
|
67
|
+
if (location?.protocol !== 'https:') return
|
|
68
|
+
|
|
69
|
+
let scheme: string
|
|
70
|
+
try {
|
|
71
|
+
scheme = new URL(url).protocol
|
|
72
|
+
} catch {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (scheme !== 'ws:') return
|
|
76
|
+
|
|
77
|
+
throw socketUnreachable(
|
|
78
|
+
`This page is served over https and the socket URL ${url} is ws://, so the browser ` +
|
|
79
|
+
'will block it as mixed content before it connects. Use the node\'s wss:// URL ' +
|
|
80
|
+
'(port 2082 by default), or serve this page over http while developing.',
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The causes a browser cannot tell apart when a handshake fails, with the fix for each. */
|
|
85
|
+
export function opaqueSocketFailureMessage(url: string): string {
|
|
86
|
+
return (
|
|
87
|
+
`Could not open the Geeq node socket at ${url}. A browser reports a failed handshake ` +
|
|
88
|
+
'with no detail, so this is one of four things: (1) the node\'s TLS certificate is ' +
|
|
89
|
+
'not trusted, which is common because nodes are addressed by bare IP — open the ' +
|
|
90
|
+
"node's https URL in a tab to see the interstitial; (2) this page is https and the " +
|
|
91
|
+
'socket is ws://, which browsers block; (3) the URL is wrong — the path is /ws/, with ' +
|
|
92
|
+
'the trailing slash, on the same port as the HTTP API; or (4) the node is down.'
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single place the SDK reaches into the WebAssembly engine.
|
|
3
|
+
*
|
|
4
|
+
* Every binding is imported by name and re-exported as a real identifier, never
|
|
5
|
+
* looked up by string. A string-keyed lookup here would break under any consumer
|
|
6
|
+
* minifier that renames module bindings, and it would fail silently for exactly
|
|
7
|
+
* one code path — the failure mode this indirection exists to prevent.
|
|
8
|
+
*/
|
|
9
|
+
export {
|
|
10
|
+
Keypair as WasmKeypair,
|
|
11
|
+
WalletAddress as WasmWalletAddress,
|
|
12
|
+
RecordType as WasmRecordType,
|
|
13
|
+
CoinType as WasmCoinType,
|
|
14
|
+
verify as wasmVerify,
|
|
15
|
+
mnemonicToSeed as wasmMnemonicToSeed,
|
|
16
|
+
generateSeedId as wasmGenerateSeedId,
|
|
17
|
+
// A proxy's account id, derived from its admin's public key and an index exactly as the
|
|
18
|
+
// Geeq wallet derives it. See src/tx/proxy.ts.
|
|
19
|
+
generateIrn as wasmGenerateIrn,
|
|
20
|
+
IrnRecordType as WasmIrnRecordType,
|
|
21
|
+
// Transaction canonicalisation. The engine cannot *build* a transaction — every
|
|
22
|
+
// transaction class it exposes has a private constructor — but it is the only
|
|
23
|
+
// thing that can turn a Cap'n Proto message into the flattened byte form the
|
|
24
|
+
// chain signs over, so structures are built with capnp and flattened here.
|
|
25
|
+
Payload as WasmPayload,
|
|
26
|
+
// Both forms of the transaction are needed and they are NOT interchangeable. The
|
|
27
|
+
// engine flattens `UserAppTxData` without the signature — that is the preimage the
|
|
28
|
+
// signature is made over — and `UserAppTx` with it, which is what the chain hashes to
|
|
29
|
+
// identify the transaction. See src/tx/attestation.ts.
|
|
30
|
+
UserAppTx as WasmUserAppTx,
|
|
31
|
+
UserAppTxData as WasmUserAppTxData,
|
|
32
|
+
// The validation-layer transaction — coin transfers — has the same two forms, but the
|
|
33
|
+
// node identifies it by the Data (signature-excluded) flatten, not the signed one.
|
|
34
|
+
// See src/tx/transfer.ts.
|
|
35
|
+
UserValTx as WasmUserValTx,
|
|
36
|
+
UserValTxData as WasmUserValTxData,
|
|
37
|
+
AttestationSubgroupData as WasmAttestationSubgroupData,
|
|
38
|
+
} from '../wasm/browser/geeq_core'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { GeeqError, notInitialized } from '../errors'
|
|
2
|
+
import type { InitOptions } from '../types'
|
|
3
|
+
import { loadWasm } from './wasm-loader.browser'
|
|
4
|
+
|
|
5
|
+
let inflight: Promise<void> | null = null
|
|
6
|
+
let ready = false
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load the Geeq engine. Idempotent and single-flight: concurrent callers share one
|
|
10
|
+
* load, and repeat calls after success resolve immediately.
|
|
11
|
+
*
|
|
12
|
+
* You rarely need to call this yourself — every async entry point in the SDK awaits
|
|
13
|
+
* it first. Call it at startup if you would rather pay the cost up front.
|
|
14
|
+
*/
|
|
15
|
+
export function initGeeq(options: InitOptions = {}): Promise<void> {
|
|
16
|
+
if (ready) return Promise.resolve()
|
|
17
|
+
|
|
18
|
+
if (inflight === null) {
|
|
19
|
+
inflight = loadWasm(options.wasm)
|
|
20
|
+
.then(() => {
|
|
21
|
+
ready = true
|
|
22
|
+
})
|
|
23
|
+
.catch((cause: unknown) => {
|
|
24
|
+
// Clear the cached promise so a later call can retry with, say, an
|
|
25
|
+
// explicit `wasm` source rather than being stuck with the first failure.
|
|
26
|
+
inflight = null
|
|
27
|
+
throw new GeeqError(
|
|
28
|
+
'init-failed',
|
|
29
|
+
'Could not load the Geeq WebAssembly engine. If your bundler does not emit ' +
|
|
30
|
+
'geeq_core_bg.wasm automatically, pass its URL as initGeeq({ wasm }).',
|
|
31
|
+
{ cause },
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return inflight
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True once the engine is loaded and the synchronous APIs are usable. */
|
|
40
|
+
export function isGeeqReady(): boolean {
|
|
41
|
+
return ready
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @internal Guard for synchronous APIs that require a loaded engine. */
|
|
45
|
+
export function assertReady(what: string): void {
|
|
46
|
+
if (!ready) {
|
|
47
|
+
throw notInitialized(what)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import initWasm from '../wasm/browser/geeq_core'
|
|
2
|
+
import type { WasmSource } from '../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Browser loader: finds the engine's .wasm file and starts it.
|
|
6
|
+
*
|
|
7
|
+
* If the app passes its own `source`, that is used. Otherwise it is the .wasm file
|
|
8
|
+
* that ships next to this script in `dist/browser/`.
|
|
9
|
+
*
|
|
10
|
+
* `new URL('./geeq_core_bg.wasm', import.meta.url)` means "geeq_core_bg.wasm in the
|
|
11
|
+
* same folder as this file". Keep it written exactly this way: Vite, webpack, Rollup
|
|
12
|
+
* and Parcel spot this pattern and copy the .wasm into the app's build for you.
|
|
13
|
+
*
|
|
14
|
+
* The Node build swaps this module for `wasm-loader.node.ts` at bundle time.
|
|
15
|
+
*/
|
|
16
|
+
export async function loadWasm(source?: WasmSource): Promise<void> {
|
|
17
|
+
const wasm = source ?? new URL('./geeq_core_bg.wasm', import.meta.url)
|
|
18
|
+
|
|
19
|
+
// `module_or_path` is the option name wasm-bindgen expects. Passing `wasm`
|
|
20
|
+
// directly instead also works, but prints a deprecation warning.
|
|
21
|
+
await initWasm({ module_or_path: wasm })
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import initWasm from '../wasm/browser/geeq_core'
|
|
3
|
+
import type { WasmSource } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Node loader: same job as the browser loader, but Node's `fetch` cannot read
|
|
7
|
+
* local files, so the .wasm is read off disk here and passed in as bytes.
|
|
8
|
+
* Used by the Node build; the test suite swaps in its own stub (see jest.config.js).
|
|
9
|
+
*/
|
|
10
|
+
export async function loadWasm(source?: WasmSource): Promise<void> {
|
|
11
|
+
const wasm = source ?? (await readFile(new URL('./geeq_core_bg.wasm', import.meta.url)))
|
|
12
|
+
|
|
13
|
+
// `module_or_path` is the option name wasm-bindgen expects. Passing `wasm`
|
|
14
|
+
// directly instead also works, but prints a deprecation warning.
|
|
15
|
+
await initWasm({ module_or_path: wasm })
|
|
16
|
+
}
|