@chaindaddy/js 0.3.4 → 0.3.5
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/api.d.ts +207 -3
- package/dist/cd.js +1 -1
- package/dist/cd.js.sri +1 -1
- package/dist/index.js +1 -1
- package/dist/verify.d.ts +120 -3
- package/dist/verify.js +246 -0
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -27,6 +27,50 @@ export interface paths {
|
|
|
27
27
|
patch?: never;
|
|
28
28
|
trace?: never;
|
|
29
29
|
};
|
|
30
|
+
"/api/v2/public/signed/coins/{chain}/{symbol}": {
|
|
31
|
+
parameters: {
|
|
32
|
+
query?: never;
|
|
33
|
+
header?: never;
|
|
34
|
+
path?: never;
|
|
35
|
+
cookie?: never;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Get a coin's registration, signed
|
|
39
|
+
* @description **Not available yet.** It answers 404 until Chain Daddy turns it on; the record on
|
|
40
|
+
* `chaindaddy.eth` and `verifyAnswer` arrive with it.
|
|
41
|
+
*
|
|
42
|
+
* Whether a ticker is registered on a chain and which wallet holds the registration, signed by
|
|
43
|
+
* Chain Daddy so you can check it wherever you got it from: a cache, a gateway, or another site's
|
|
44
|
+
* server. `jws` is the signed answer (a compact JWS); `claims` repeats what it says so you can read
|
|
45
|
+
* it, but trust only what you get out of `jws` after verifying it.
|
|
46
|
+
*
|
|
47
|
+
* `status` is `registered` (the ticker has a verified page on that chain), `not_registered` (it
|
|
48
|
+
* has none) or `removed` (it has one and Chain Daddy took it down). All three answer 200 and are
|
|
49
|
+
* signed, so nobody between you and Chain Daddy can make up a missing registration. Only
|
|
50
|
+
* `registered` carries `tokenAddress`, `crownId`, `holder`, `pageUrl` and, when known,
|
|
51
|
+
* `registeredAt`.
|
|
52
|
+
*
|
|
53
|
+
* To verify: split `jws` at the dots; the first part is the header (`alg` must be `EdDSA`, and
|
|
54
|
+
* `kid` names the key), the second the claims. Check the Ed25519 signature over the first two
|
|
55
|
+
* parts against the key with that `kid`, then check `typ` is `cd-answer+v1`, `iss` is
|
|
56
|
+
* `https://chaindaddy.io` and `exp` has not passed. `verifyAnswer` in `@chaindaddy/js/verify` does
|
|
57
|
+
* all of it. The keys are at `GET /api/v2/iap/jwks` (the keys receipts are signed with). They are
|
|
58
|
+
* also named on `chaindaddy.eth`, in the text record `io.chaindaddy.signing-keys`: `ed25519:<x>`
|
|
59
|
+
* entries separated by a space, the current key first, where `<x>` is the key's `x` in the key
|
|
60
|
+
* set. Read the record yourself and you do not depend on this API for the keys either.
|
|
61
|
+
*
|
|
62
|
+
* An answer is valid for 15 minutes (`exp`), and caches keep it for less than that. Keyless, the
|
|
63
|
+
* same for every caller.
|
|
64
|
+
*/
|
|
65
|
+
get: operations["getPublicSignedCoin"];
|
|
66
|
+
put?: never;
|
|
67
|
+
post?: never;
|
|
68
|
+
delete?: never;
|
|
69
|
+
options?: never;
|
|
70
|
+
head?: never;
|
|
71
|
+
patch?: never;
|
|
72
|
+
trace?: never;
|
|
73
|
+
};
|
|
30
74
|
"/api/v2/public/coins/{chain}/{symbol}/apps": {
|
|
31
75
|
parameters: {
|
|
32
76
|
query?: never;
|
|
@@ -138,7 +182,8 @@ export interface paths {
|
|
|
138
182
|
/**
|
|
139
183
|
* Receipt signing keys (JWKS)
|
|
140
184
|
* @description The Ed25519 public keys receipts are signed with (the current key and the previous one), as a JSON Web Key Set.
|
|
141
|
-
* Match a receipt's `kid` header to a key and verify its EdDSA signature offline.
|
|
185
|
+
* Match a receipt's `kid` header to a key and verify its EdDSA signature offline. Signed answers
|
|
186
|
+
* (`GET /api/v2/public/signed/coins/{chain}/{symbol}`) are signed with the same keys.
|
|
142
187
|
*/
|
|
143
188
|
get: operations["getIapJwks"];
|
|
144
189
|
put?: never;
|
|
@@ -1496,6 +1541,73 @@ export interface components {
|
|
|
1496
1541
|
/** @description Machine-readable error code */
|
|
1497
1542
|
code?: string;
|
|
1498
1543
|
};
|
|
1544
|
+
/** @description A signed answer and what it says. Trust `claims` only as read out of `jws` after verifying it. */
|
|
1545
|
+
PublicSignedAnswer: {
|
|
1546
|
+
/** @description The signed answer: a compact JWS (header, claims and signature, base64url, joined by dots), signed with Ed25519 (`alg` `EdDSA`) by the key its header's `kid` names */
|
|
1547
|
+
jws: string;
|
|
1548
|
+
claims: components["schemas"]["SignedAnswerClaims"];
|
|
1549
|
+
};
|
|
1550
|
+
/** @description What a signed answer says. The same object is the payload of `jws`. */
|
|
1551
|
+
SignedAnswerClaims: {
|
|
1552
|
+
/**
|
|
1553
|
+
* @description Who signed it. Always `https://chaindaddy.io`.
|
|
1554
|
+
* @example https://chaindaddy.io
|
|
1555
|
+
*/
|
|
1556
|
+
iss: string;
|
|
1557
|
+
/**
|
|
1558
|
+
* @description What kind of signed document this is. Always `cd-answer+v1`; a store receipt, signed by the same keys, is `iap-receipt+v1`.
|
|
1559
|
+
* @enum {string}
|
|
1560
|
+
*/
|
|
1561
|
+
typ: "cd-answer+v1";
|
|
1562
|
+
/**
|
|
1563
|
+
* Format: int64
|
|
1564
|
+
* @description When it was signed, in Unix seconds
|
|
1565
|
+
*/
|
|
1566
|
+
iat: number;
|
|
1567
|
+
/**
|
|
1568
|
+
* Format: int64
|
|
1569
|
+
* @description When it stops being valid, in Unix seconds, 15 minutes after `iat`. Do not act on it after this.
|
|
1570
|
+
*/
|
|
1571
|
+
exp: number;
|
|
1572
|
+
/**
|
|
1573
|
+
* @description What the answer is about. `registration` is a ticker's registration on a chain.
|
|
1574
|
+
* @enum {string}
|
|
1575
|
+
*/
|
|
1576
|
+
kind: "registration";
|
|
1577
|
+
/**
|
|
1578
|
+
* @description CAIP-2 chain ID
|
|
1579
|
+
* @example eip155:8453
|
|
1580
|
+
*/
|
|
1581
|
+
chainId: string;
|
|
1582
|
+
/**
|
|
1583
|
+
* @description Chain key
|
|
1584
|
+
* @example base
|
|
1585
|
+
*/
|
|
1586
|
+
chain: string;
|
|
1587
|
+
/** @description The ticker: as registered when `status` is `registered`, otherwise the one asked about, in upper case. Tickers match in any case, so compare without regard to case. */
|
|
1588
|
+
symbol: string;
|
|
1589
|
+
/**
|
|
1590
|
+
* @description `registered`: the ticker has a verified page on the chain. `not_registered`: it has none. `removed`: it has one, and Chain Daddy took it down.
|
|
1591
|
+
* @enum {string}
|
|
1592
|
+
*/
|
|
1593
|
+
status: "registered" | "not_registered" | "removed";
|
|
1594
|
+
/** @description The registered token's address (lowercase on EVM chains). Only when `status` is `registered`. */
|
|
1595
|
+
tokenAddress?: string;
|
|
1596
|
+
/**
|
|
1597
|
+
* Format: int64
|
|
1598
|
+
* @description The registration's ID on that chain. Only when `status` is `registered`.
|
|
1599
|
+
*/
|
|
1600
|
+
crownId?: number;
|
|
1601
|
+
/** @description The wallet that holds the registration (lowercase on EVM chains). Only when `status` is `registered`. */
|
|
1602
|
+
holder?: string;
|
|
1603
|
+
/**
|
|
1604
|
+
* Format: date-time
|
|
1605
|
+
* @description When the registration was made. Only when `status` is `registered`, and left out when not known.
|
|
1606
|
+
*/
|
|
1607
|
+
registeredAt?: string;
|
|
1608
|
+
/** @description The coin's page on chaindaddy.io. Only when `status` is `registered`. */
|
|
1609
|
+
pageUrl?: string;
|
|
1610
|
+
};
|
|
1499
1611
|
IapError: {
|
|
1500
1612
|
/** @description Human-readable description */
|
|
1501
1613
|
error: string;
|
|
@@ -3147,6 +3259,96 @@ export interface operations {
|
|
|
3147
3259
|
};
|
|
3148
3260
|
};
|
|
3149
3261
|
};
|
|
3262
|
+
getPublicSignedCoin: {
|
|
3263
|
+
parameters: {
|
|
3264
|
+
query?: never;
|
|
3265
|
+
header?: never;
|
|
3266
|
+
path: {
|
|
3267
|
+
/** @description Chain key, lowercase, as in the page's own address (e.g. `base`, `solana`). A numeric chain ID answers 404. */
|
|
3268
|
+
chain: string;
|
|
3269
|
+
/** @description The coin's ticker (letters and digits, any case) */
|
|
3270
|
+
symbol: string;
|
|
3271
|
+
};
|
|
3272
|
+
cookie?: never;
|
|
3273
|
+
};
|
|
3274
|
+
requestBody?: never;
|
|
3275
|
+
responses: {
|
|
3276
|
+
/** @description The signed answer, whatever the status */
|
|
3277
|
+
200: {
|
|
3278
|
+
headers: {
|
|
3279
|
+
[name: string]: unknown;
|
|
3280
|
+
};
|
|
3281
|
+
content: {
|
|
3282
|
+
"application/json": components["schemas"]["PublicSignedAnswer"];
|
|
3283
|
+
};
|
|
3284
|
+
};
|
|
3285
|
+
/** @description The request came with the user agent of an AI or LLM crawler, and no cached answer was there to serve instead. Never sent to a browser or to a server using its own user agent. */
|
|
3286
|
+
403: {
|
|
3287
|
+
headers: {
|
|
3288
|
+
[name: string]: unknown;
|
|
3289
|
+
};
|
|
3290
|
+
content: {
|
|
3291
|
+
/**
|
|
3292
|
+
* @example {
|
|
3293
|
+
* "error": "ai_bot_blocked",
|
|
3294
|
+
* "message": "AI/LLM scraping is not permitted. Use our API with a developer-tier API key for programmatic access."
|
|
3295
|
+
* }
|
|
3296
|
+
*/
|
|
3297
|
+
"application/json": components["schemas"]["Error"];
|
|
3298
|
+
};
|
|
3299
|
+
};
|
|
3300
|
+
/** @description An unknown chain, or a ticker that is not letters and digits. Not signed; there is nothing to attest. */
|
|
3301
|
+
404: {
|
|
3302
|
+
headers: {
|
|
3303
|
+
[name: string]: unknown;
|
|
3304
|
+
};
|
|
3305
|
+
content: {
|
|
3306
|
+
/**
|
|
3307
|
+
* @example {
|
|
3308
|
+
* "error": "not_found",
|
|
3309
|
+
* "code": "not_found",
|
|
3310
|
+
* "message": "Not found."
|
|
3311
|
+
* }
|
|
3312
|
+
*/
|
|
3313
|
+
"application/json": components["schemas"]["Error"];
|
|
3314
|
+
};
|
|
3315
|
+
};
|
|
3316
|
+
/** @description More than 400 requests in a minute from one IP address, across the whole API. Never cached. */
|
|
3317
|
+
429: {
|
|
3318
|
+
headers: {
|
|
3319
|
+
/** @description Seconds to wait before trying again */
|
|
3320
|
+
"Retry-After"?: number;
|
|
3321
|
+
[name: string]: unknown;
|
|
3322
|
+
};
|
|
3323
|
+
content: {
|
|
3324
|
+
/**
|
|
3325
|
+
* @example {
|
|
3326
|
+
* "error": "Too many requests. Please try again later.",
|
|
3327
|
+
* "code": "rate_limit_exceeded",
|
|
3328
|
+
* "message": "Too many requests. Please try again later."
|
|
3329
|
+
* }
|
|
3330
|
+
*/
|
|
3331
|
+
"application/json": components["schemas"]["Error"];
|
|
3332
|
+
};
|
|
3333
|
+
};
|
|
3334
|
+
/** @description The answer could not be read or signed right now. Never cached, never signed; try again. */
|
|
3335
|
+
503: {
|
|
3336
|
+
headers: {
|
|
3337
|
+
[name: string]: unknown;
|
|
3338
|
+
};
|
|
3339
|
+
content: {
|
|
3340
|
+
/**
|
|
3341
|
+
* @example {
|
|
3342
|
+
* "error": "unavailable",
|
|
3343
|
+
* "code": "unavailable",
|
|
3344
|
+
* "message": "Unavailable right now. Try again."
|
|
3345
|
+
* }
|
|
3346
|
+
*/
|
|
3347
|
+
"application/json": components["schemas"]["Error"];
|
|
3348
|
+
};
|
|
3349
|
+
};
|
|
3350
|
+
};
|
|
3351
|
+
};
|
|
3150
3352
|
listPublicCoinApps: {
|
|
3151
3353
|
parameters: {
|
|
3152
3354
|
query?: never;
|
|
@@ -5231,7 +5433,8 @@ export interface operations {
|
|
|
5231
5433
|
};
|
|
5232
5434
|
/**
|
|
5233
5435
|
* @description `STORE_KEY_REQUIRED`: called with a signed-in session instead of a store key. `SCOPE_REQUIRED`: the key
|
|
5234
|
-
* lacks `orders:write`. `STORE_NOT_LIVE`: the store is paused or its plan lapsed
|
|
5436
|
+
* lacks `orders:write`. `STORE_NOT_LIVE`: the store is paused or its plan lapsed, or (`reason`
|
|
5437
|
+
* `owner_changed`) its page has a new owner who has not opened it yet. `SELLER_TERMS_REQUIRED`:
|
|
5235
5438
|
* the wallet that opened the store has not signed the Seller Terms
|
|
5236
5439
|
*/
|
|
5237
5440
|
403: {
|
|
@@ -5447,7 +5650,8 @@ export interface operations {
|
|
|
5447
5650
|
/**
|
|
5448
5651
|
* @description `STORE_KEY_REQUIRED`: called with a signed-in session instead of a store key. `SCOPE_REQUIRED`: the
|
|
5449
5652
|
* key lacks `orders:write`. `CHECKOUT_SESSION_WALLET`: the session names another wallet.
|
|
5450
|
-
* `IAP_PLAN_REQUIRED` or `STORE_NOT_LIVE`: the store's plan lapsed or the store is paused
|
|
5653
|
+
* `IAP_PLAN_REQUIRED` or `STORE_NOT_LIVE`: the store's plan lapsed or the store is paused, or (`reason`
|
|
5654
|
+
* `owner_changed`) its page has a new owner who has not opened it yet.
|
|
5451
5655
|
* `SESSION_REQUIRED`: the session's item belongs to an installed app
|
|
5452
5656
|
*/
|
|
5453
5657
|
403: {
|
package/dist/cd.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";(()=>{var R="0.3.
|
|
1
|
+
"use strict";(()=>{var R="0.3.5";function C(t){let e="";for(let i of t)e+=String.fromCharCode(i);return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function w(){return C(crypto.getRandomValues(new Uint8Array(32)))}function T(t,e,i,o,r){let u=n=>{if(n&&typeof n=="object"&&n.type===t)try{i(n)}catch{}},l=n=>{n.origin===e&&(!r||n.source===r)&&u(n.data)};window.addEventListener("message",l);let a=null;if(o&&typeof BroadcastChannel=="function"&&e===location.origin)try{a=new BroadcastChannel(o),a.onmessage=n=>u(n.data)}catch{a=null}return()=>{window.removeEventListener("message",l),a?.close()}}var y={get(t){try{return sessionStorage.getItem(t)}catch{return null}},set(t,e){try{sessionStorage.setItem(t,e)}catch{}},remove(t){try{sessionStorage.removeItem(t)}catch{}}};var F="https://api.chaindaddy.io",G={"https://api.chaindaddy.io":"https://chaindaddy.io/_oauth/authorize","https://staging-api.chaindaddy.io":"https://staging.chaindaddy.io/_oauth/authorize"},b="cd-signin",U="cd:signin",v="cd.signin",_=300;async function K(t){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return C(new Uint8Array(e))}function W(t="openid"){let e=t.split(/\s+/).filter(Boolean);return(e.includes("openid")?e:["openid",...e]).join(" ")}async function Y(t,e,i){let o=new URLSearchParams({response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUri,scope:W(t.scope),state:e.state,nonce:e.nonce,code_challenge:await K(e.verifier),code_challenge_method:"S256"});return t.claims&&o.set("claims",typeof t.claims=="string"?t.claims:JSON.stringify(t.claims)),`${i}?${o}`}function A(t,e){return{code:e,codeVerifier:t.verifier,state:t.state,nonce:t.nonce,issuer:t.issuer,clientId:t.clientId,redirectUri:t.redirectUri}}var m;function k(t){let e=(t.issuer??F).replace(/\/$/,""),i=t.authorizeUrl??G[e];if(!i)return Promise.reject(new Error("authorizeUrl is required for this issuer"));if(!t.clientId||!t.redirectUri)return Promise.reject(new Error("clientId and redirectUri are required"));if(m&&!m.popup.closed)return m.popup.focus(),m.waiting;let o={state:w(),nonce:w(),verifier:w(),issuer:e,clientId:t.clientId,redirectUri:t.redirectUri};y.set(v,JSON.stringify(o));let r=window.open("",b,"popup,width=480,height=720"),u=(async()=>{let l=await Y(t,o,i);if(!r)return location.assign(l),null;r.location.href=l;let a=new URL(t.redirectUri,location.href).origin;return new Promise((n,c)=>{let s,d=()=>{g(),clearInterval(h),clearTimeout(s)},g=T(U,a,p=>{p.state===o.state&&(d(),y.remove(v),typeof p.error=="string"&&p.error?c(new Error(p.error)):p.iss!==e?c(new Error("the answer came from another issuer")):typeof p.code!="string"||!p.code?c(new Error("the answer carried no code")):n(A(o,p.code)))},b,r),h=setInterval(()=>{r.closed&&(clearInterval(h),s=setTimeout(()=>{d(),c(new Error("closed"))},_))},_);t.signal?.addEventListener("abort",()=>{d(),c(new Error("aborted"))})})})();if(r){m={popup:r,waiting:u};let l=()=>{m&&m.popup===r&&(m=void 0)};u.then(l,l)}return u}async function L(){let t=new URLSearchParams(location.search),e={type:U,code:t.get("code"),state:t.get("state"),iss:t.get("iss"),error:t.get("error")};if(window.name===b){try{window.opener?.postMessage(e,location.origin)}catch{}try{let r=new BroadcastChannel(b);r.postMessage(e),r.close()}catch{}return window.close(),null}let i=y.get(v);y.remove(v);let o=i?JSON.parse(i):null;if(!o||e.state!==o.state)throw new Error("the sign-in state does not match: start again");if(e.error)throw new Error(e.error);if(e.iss!==o.issuer)throw new Error("the answer came from another issuer");if(!e.code)throw new Error("the answer carried no code");return A(o,e.code)}async function M(t){let e=await fetch(`${t.issuer}/api/v2/oauth/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:t.code,redirect_uri:t.redirectUri,client_id:t.clientId,code_verifier:t.codeVerifier})}),i=await e.json();if(!e.ok)throw new Error(`${i.error??e.status}: ${i.error_description??""}`.trim());return i}var J='<svg viewBox="0 0 24 24" fill="#FFD700" width="20" height="20" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5z" stroke="#B8860B" stroke-width="0.5"/><path d="M5 18h14v2H5v-2z" stroke="#B8860B" stroke-width="0.5"/></svg>',X=`
|
|
2
2
|
:host { display: inline-block; }
|
|
3
3
|
button { display: inline-flex; align-items: center; justify-content: center; gap: 10px; box-sizing: border-box;
|
|
4
4
|
min-height: 40px; padding: 0 16px; border-radius: 10px; font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
|
package/dist/cd.js.sri
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
sha384-
|
|
1
|
+
sha384-yNIDKP+1edth+ChOSlYVUWmXdlE3j2HVn66BpXQ0x1J4Bs87EUmozo6w2bxKCXFZ
|
package/dist/index.js
CHANGED
package/dist/verify.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare const MIN_CONFIRMATIONS: Readonly<Record<string, number>>;
|
|
|
13
13
|
/** `token` of an order paid in the chain's own coin (ETH on Base, POL on Polygon). */
|
|
14
14
|
export declare const NATIVE_TOKEN = "0x0000000000000000000000000000000000000000";
|
|
15
15
|
export type VerifyErrorCode =
|
|
16
|
-
/** A Solana order: follow the manual recipe for now. */
|
|
16
|
+
/** A Solana order: follow the manual recipe for now. Or, from verifyAnswer, a runtime whose Web Crypto has no Ed25519. */
|
|
17
17
|
'unsupported'
|
|
18
18
|
/** What you passed (options, expect) is missing or malformed. */
|
|
19
19
|
| 'bad_options'
|
|
@@ -26,8 +26,24 @@ export type VerifyErrorCode =
|
|
|
26
26
|
/** The block is not the order, asset, payout address or amount you expect. */
|
|
27
27
|
| 'expectation_mismatch'
|
|
28
28
|
/** The RPC endpoint failed or answered something that is not an answer. */
|
|
29
|
-
| 'rpc_error'
|
|
30
|
-
/**
|
|
29
|
+
| 'rpc_error'
|
|
30
|
+
/** verifyAnswer: not a compact JWS, not signed with EdDSA, not a signed answer (a receipt, say), not from the issuer you expect, or missing a claim an answer carries. */
|
|
31
|
+
| 'bad_answer'
|
|
32
|
+
/** verifyAnswer: the answer names a key that is not one of the keys. */
|
|
33
|
+
| 'unknown_key'
|
|
34
|
+
/** verifyAnswer: the signature is not that key's signature over this answer. */
|
|
35
|
+
| 'bad_signature'
|
|
36
|
+
/** verifyAnswer: a genuine answer past its `exp`. Fetch a new one. */
|
|
37
|
+
| 'expired'
|
|
38
|
+
/** The keys you passed, or the record value, are not Ed25519 keys. */
|
|
39
|
+
| 'bad_keys'
|
|
40
|
+
/** verifyAnswer: the key set could not be fetched, or holds no Ed25519 key. */
|
|
41
|
+
| 'keys_unavailable';
|
|
42
|
+
/**
|
|
43
|
+
* Every failure of verifyPayment and verifyAnswer. A payment that is simply not
|
|
44
|
+
* there yet is not an error: it resolves `paid: false`. Nor is a coin that is
|
|
45
|
+
* not registered: that is a signed answer with `status: "not_registered"`.
|
|
46
|
+
*/
|
|
31
47
|
export declare class VerifyError extends Error {
|
|
32
48
|
readonly code: VerifyErrorCode;
|
|
33
49
|
constructor(code: VerifyErrorCode, message: string);
|
|
@@ -124,3 +140,104 @@ export declare function isKnownCheckout(chainId: string | number, checkout: stri
|
|
|
124
140
|
* `paid: true` on a doubt.
|
|
125
141
|
*/
|
|
126
142
|
export declare function verifyPayment(verification: Verification, options: VerifyOptions): Promise<PaymentResult>;
|
|
143
|
+
/** The `typ` claim of a signed answer. A store receipt, signed by the same keys, is "iap-receipt+v1" and is refused. */
|
|
144
|
+
export declare const ANSWER_TYPE = "cd-answer+v1";
|
|
145
|
+
/** The `iss` claim of a signed answer. */
|
|
146
|
+
export declare const ANSWER_ISSUER = "https://chaindaddy.io";
|
|
147
|
+
/** Where the signing keys are published, as a JSON Web Key Set. */
|
|
148
|
+
export declare const SIGNING_KEYS_URL = "https://api.chaindaddy.io/api/v2/iap/jwks";
|
|
149
|
+
/** The text record on chaindaddy.eth that names the signing keys: `ed25519:<x>` entries separated by a space, the current key first. */
|
|
150
|
+
export declare const SIGNING_KEYS_RECORD = "io.chaindaddy.signing-keys";
|
|
151
|
+
/** One Ed25519 signing key. */
|
|
152
|
+
export interface SigningKey {
|
|
153
|
+
/** The first 16 hex characters of the SHA-256 of the raw public key: what a JWS header's `kid` names. */
|
|
154
|
+
kid: string;
|
|
155
|
+
/** The raw 32-byte public key, base64url without padding (a JWK's `x`). */
|
|
156
|
+
x: string;
|
|
157
|
+
}
|
|
158
|
+
/** What a signed answer says. */
|
|
159
|
+
export interface AnswerClaims {
|
|
160
|
+
iss: string;
|
|
161
|
+
typ: typeof ANSWER_TYPE;
|
|
162
|
+
/** Unix seconds. */
|
|
163
|
+
iat: number;
|
|
164
|
+
/** Unix seconds: 15 minutes after `iat`. */
|
|
165
|
+
exp: number;
|
|
166
|
+
/** What the answer is about. More kinds may be added. */
|
|
167
|
+
kind: 'registration' | (string & {});
|
|
168
|
+
/** CAIP-2, e.g. "eip155:8453". */
|
|
169
|
+
chainId: string;
|
|
170
|
+
/** The chain key, e.g. "base". */
|
|
171
|
+
chain: string;
|
|
172
|
+
/** The ticker: as registered when `status` is "registered", otherwise the one asked about, in upper case. */
|
|
173
|
+
symbol: string;
|
|
174
|
+
/** "registered": a verified page. "not_registered": none. "removed": one Chain Daddy took down. More may be added: treat one you do not know as not registered. */
|
|
175
|
+
status: 'registered' | 'not_registered' | 'removed' | (string & {});
|
|
176
|
+
/** The fields below are present only when `status` is "registered". Addresses are lowercase on EVM chains. */
|
|
177
|
+
tokenAddress?: string;
|
|
178
|
+
/** The registration's ID on that chain. */
|
|
179
|
+
crownId?: number;
|
|
180
|
+
/** The wallet that holds the registration. */
|
|
181
|
+
holder?: string;
|
|
182
|
+
/** RFC 3339. Left out when not known. */
|
|
183
|
+
registeredAt?: string;
|
|
184
|
+
pageUrl?: string;
|
|
185
|
+
}
|
|
186
|
+
/** What you asked about. An answer about anything else is refused. */
|
|
187
|
+
export interface AnswerExpectation {
|
|
188
|
+
/** The chain key you asked about, e.g. "base". */
|
|
189
|
+
chain?: string;
|
|
190
|
+
/** Or its CAIP-2 ID, e.g. "eip155:8453". */
|
|
191
|
+
chainId?: string;
|
|
192
|
+
/** The ticker you asked about, in any case. */
|
|
193
|
+
symbol?: string;
|
|
194
|
+
/** The kind of answer, e.g. "registration". */
|
|
195
|
+
kind?: string;
|
|
196
|
+
}
|
|
197
|
+
export interface VerifyAnswerOptions {
|
|
198
|
+
/**
|
|
199
|
+
* The keys to trust: what signingKeysFromRecord returns, or JWKs (the `keys`
|
|
200
|
+
* of the key set). A key's `kid` is always computed from the key itself.
|
|
201
|
+
*/
|
|
202
|
+
keys?: readonly (SigningKey | {
|
|
203
|
+
x: string;
|
|
204
|
+
kty?: string;
|
|
205
|
+
crv?: string;
|
|
206
|
+
})[];
|
|
207
|
+
/** Instead of keys: a key set to fetch. Default SIGNING_KEYS_URL, when neither is passed. */
|
|
208
|
+
jwksUrl?: string;
|
|
209
|
+
/**
|
|
210
|
+
* What you asked about. Pass it whenever the answer did not come straight
|
|
211
|
+
* from Chain Daddy: whoever relays answers can hand you a genuine one about
|
|
212
|
+
* a different coin.
|
|
213
|
+
*/
|
|
214
|
+
expect?: AnswerExpectation;
|
|
215
|
+
/** The `iss` to accept. Default ANSWER_ISSUER. */
|
|
216
|
+
issuer?: string;
|
|
217
|
+
/** Seconds past `exp` an answer is still accepted, for a clock that runs fast. Default 0. */
|
|
218
|
+
clockToleranceSeconds?: number;
|
|
219
|
+
/** The time to check `exp` against, in Unix seconds. Default: the clock. */
|
|
220
|
+
now?: number;
|
|
221
|
+
/** Used to fetch the key set, in place of the global fetch. */
|
|
222
|
+
fetch?: typeof fetch;
|
|
223
|
+
/** Aborts the key set request. */
|
|
224
|
+
signal?: AbortSignal;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The signing keys named by the text record SIGNING_KEYS_RECORD on
|
|
228
|
+
* chaindaddy.eth: `ed25519:<x>` entries separated by whitespace, the current
|
|
229
|
+
* key first. An entry of another kind is skipped (a new kind of key can be
|
|
230
|
+
* added to the record); an `ed25519:` entry that is not a key throws, so a
|
|
231
|
+
* damaged record is never read as fewer keys.
|
|
232
|
+
*/
|
|
233
|
+
export declare function signingKeysFromRecord(value: string): SigningKey[];
|
|
234
|
+
/**
|
|
235
|
+
* Checks a signed answer and resolves what it says. It resolves only when the
|
|
236
|
+
* JWS is signed with EdDSA by one of the keys, is a signed answer (`typ`) from
|
|
237
|
+
* the issuer you expect (`iss`), has not expired (`exp`) and, when you pass
|
|
238
|
+
* `expect`, is about what you asked. Anything else rejects with a VerifyError.
|
|
239
|
+
*
|
|
240
|
+
* A coin that is not registered resolves too, with `status: "not_registered"`:
|
|
241
|
+
* read `status` before anything else.
|
|
242
|
+
*/
|
|
243
|
+
export declare function verifyAnswer(jws: string, options?: VerifyAnswerOptions): Promise<AnswerClaims>;
|
package/dist/verify.js
CHANGED
|
@@ -330,13 +330,259 @@ async function verifyPayment(verification, options) {
|
|
|
330
330
|
const paid = payer !== NATIVE_TOKEN;
|
|
331
331
|
return { paid, payer: paid ? payer : null, blockNumber: Number(at), ...result };
|
|
332
332
|
}
|
|
333
|
+
var ANSWER_TYPE = "cd-answer+v1";
|
|
334
|
+
var ANSWER_ISSUER = "https://chaindaddy.io";
|
|
335
|
+
var SIGNING_KEYS_URL = "https://api.chaindaddy.io/api/v2/iap/jwks";
|
|
336
|
+
var SIGNING_KEYS_RECORD = "io.chaindaddy.signing-keys";
|
|
337
|
+
function fromBase64Url(v) {
|
|
338
|
+
if (typeof v !== "string" || !/^[A-Za-z0-9_-]*$/.test(v) || v.length % 4 === 1) return null;
|
|
339
|
+
let bin;
|
|
340
|
+
try {
|
|
341
|
+
bin = atob(v.replace(/-/g, "+").replace(/_/g, "/"));
|
|
342
|
+
} catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const out = new Uint8Array(bin.length);
|
|
346
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
347
|
+
return toBase64Url(out) === v ? out : null;
|
|
348
|
+
}
|
|
349
|
+
function toBase64Url(bytes) {
|
|
350
|
+
let bin = "";
|
|
351
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
352
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
353
|
+
}
|
|
354
|
+
var SHA256_K = new Uint32Array([
|
|
355
|
+
1116352408,
|
|
356
|
+
1899447441,
|
|
357
|
+
3049323471,
|
|
358
|
+
3921009573,
|
|
359
|
+
961987163,
|
|
360
|
+
1508970993,
|
|
361
|
+
2453635748,
|
|
362
|
+
2870763221,
|
|
363
|
+
3624381080,
|
|
364
|
+
310598401,
|
|
365
|
+
607225278,
|
|
366
|
+
1426881987,
|
|
367
|
+
1925078388,
|
|
368
|
+
2162078206,
|
|
369
|
+
2614888103,
|
|
370
|
+
3248222580,
|
|
371
|
+
3835390401,
|
|
372
|
+
4022224774,
|
|
373
|
+
264347078,
|
|
374
|
+
604807628,
|
|
375
|
+
770255983,
|
|
376
|
+
1249150122,
|
|
377
|
+
1555081692,
|
|
378
|
+
1996064986,
|
|
379
|
+
2554220882,
|
|
380
|
+
2821834349,
|
|
381
|
+
2952996808,
|
|
382
|
+
3210313671,
|
|
383
|
+
3336571891,
|
|
384
|
+
3584528711,
|
|
385
|
+
113926993,
|
|
386
|
+
338241895,
|
|
387
|
+
666307205,
|
|
388
|
+
773529912,
|
|
389
|
+
1294757372,
|
|
390
|
+
1396182291,
|
|
391
|
+
1695183700,
|
|
392
|
+
1986661051,
|
|
393
|
+
2177026350,
|
|
394
|
+
2456956037,
|
|
395
|
+
2730485921,
|
|
396
|
+
2820302411,
|
|
397
|
+
3259730800,
|
|
398
|
+
3345764771,
|
|
399
|
+
3516065817,
|
|
400
|
+
3600352804,
|
|
401
|
+
4094571909,
|
|
402
|
+
275423344,
|
|
403
|
+
430227734,
|
|
404
|
+
506948616,
|
|
405
|
+
659060556,
|
|
406
|
+
883997877,
|
|
407
|
+
958139571,
|
|
408
|
+
1322822218,
|
|
409
|
+
1537002063,
|
|
410
|
+
1747873779,
|
|
411
|
+
1955562222,
|
|
412
|
+
2024104815,
|
|
413
|
+
2227730452,
|
|
414
|
+
2361852424,
|
|
415
|
+
2428436474,
|
|
416
|
+
2756734187,
|
|
417
|
+
3204031479,
|
|
418
|
+
3329325298
|
|
419
|
+
]);
|
|
420
|
+
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
421
|
+
function keyId(key) {
|
|
422
|
+
const block = new Uint8Array(64);
|
|
423
|
+
block.set(key);
|
|
424
|
+
block[32] = 128;
|
|
425
|
+
block[62] = 1;
|
|
426
|
+
const view = new DataView(block.buffer);
|
|
427
|
+
const w = new Uint32Array(64);
|
|
428
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(i * 4);
|
|
429
|
+
for (let i = 16; i < 64; i++) {
|
|
430
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
|
|
431
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
|
|
432
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
|
433
|
+
}
|
|
434
|
+
const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
|
|
435
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
436
|
+
for (let i = 0; i < 64; i++) {
|
|
437
|
+
const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + SHA256_K[i] + w[i] >>> 0;
|
|
438
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
439
|
+
hh = g;
|
|
440
|
+
g = f;
|
|
441
|
+
f = e;
|
|
442
|
+
e = d + t1 >>> 0;
|
|
443
|
+
d = c;
|
|
444
|
+
c = b;
|
|
445
|
+
b = a;
|
|
446
|
+
a = t1 + t2 >>> 0;
|
|
447
|
+
}
|
|
448
|
+
return [a, b].map((v, i) => (h[i] + v >>> 0).toString(16).padStart(8, "0")).join("");
|
|
449
|
+
}
|
|
450
|
+
function keyFromX(x) {
|
|
451
|
+
const raw = typeof x === "string" && x.length === 43 ? fromBase64Url(x) : null;
|
|
452
|
+
return raw && raw.length === 32 ? { kid: keyId(raw), x, raw } : null;
|
|
453
|
+
}
|
|
454
|
+
function keyFromObject(v) {
|
|
455
|
+
if (!isObject(v)) return null;
|
|
456
|
+
if (v.kty !== void 0 && v.kty !== "OKP" || v.crv !== void 0 && v.crv !== "Ed25519") return null;
|
|
457
|
+
return keyFromX(v.x);
|
|
458
|
+
}
|
|
459
|
+
function signingKeysFromRecord(value) {
|
|
460
|
+
if (typeof value !== "string") throw new VerifyError("bad_keys", "the record value is not a string");
|
|
461
|
+
const out = [];
|
|
462
|
+
for (const entry of value.split(/\s+/)) {
|
|
463
|
+
if (!entry.startsWith("ed25519:")) continue;
|
|
464
|
+
const key = keyFromX(entry.slice("ed25519:".length));
|
|
465
|
+
if (!key) throw new VerifyError("bad_keys", `"${entry.slice(0, 60)}" is not an Ed25519 key: expected ed25519: and 43 base64url characters`);
|
|
466
|
+
if (!out.some((k) => k.kid === key.kid)) out.push({ kid: key.kid, x: key.x });
|
|
467
|
+
}
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
async function fetchKeys(url, o) {
|
|
471
|
+
const doFetch = o.fetch ?? globalThis.fetch;
|
|
472
|
+
if (typeof doFetch !== "function") throw new VerifyError("bad_options", "no fetch here: pass options.fetch, or options.keys");
|
|
473
|
+
let body;
|
|
474
|
+
try {
|
|
475
|
+
const res = await doFetch(url, { headers: { accept: "application/json" }, signal: o.signal });
|
|
476
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
477
|
+
body = await res.json();
|
|
478
|
+
} catch (e) {
|
|
479
|
+
throw new VerifyError("keys_unavailable", `the key set at ${url} could not be read: ${e instanceof Error ? e.message : String(e)}`);
|
|
480
|
+
}
|
|
481
|
+
const keys = isObject(body) && Array.isArray(body.keys) ? body.keys.map(keyFromObject).filter((k) => k !== null) : [];
|
|
482
|
+
if (keys.length === 0) throw new VerifyError("keys_unavailable", `the key set at ${url} holds no Ed25519 key`);
|
|
483
|
+
return keys;
|
|
484
|
+
}
|
|
485
|
+
function passedKeys(v) {
|
|
486
|
+
if (!Array.isArray(v) || v.length === 0) throw new VerifyError("bad_keys", "options.keys must be a list of keys, one or more");
|
|
487
|
+
return Array.from(v, (k, i) => {
|
|
488
|
+
const key = keyFromObject(k);
|
|
489
|
+
if (!key) throw new VerifyError("bad_keys", `options.keys[${i}] is not an Ed25519 key (x: 43 base64url characters)`);
|
|
490
|
+
return key;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
function parseAnswerExpectation(v) {
|
|
494
|
+
if (v === void 0) return null;
|
|
495
|
+
const fields = ["chain", "chainId", "symbol", "kind"];
|
|
496
|
+
if (!isObject(v) || Object.keys(v).length === 0 || Object.keys(v).some((k) => !fields.includes(k) || typeof v[k] !== "string" || v[k] === "")) {
|
|
497
|
+
throw new VerifyError("bad_options", "options.expect takes chain or chainId, symbol and kind, each a string: what you asked about");
|
|
498
|
+
}
|
|
499
|
+
return v;
|
|
500
|
+
}
|
|
501
|
+
function jsonObject(bytes) {
|
|
502
|
+
try {
|
|
503
|
+
const v = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
504
|
+
return isObject(v) ? v : null;
|
|
505
|
+
} catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function ed25519Verify(key, signature, data) {
|
|
510
|
+
const subtle = globalThis.crypto?.subtle;
|
|
511
|
+
if (!subtle) throw new VerifyError("unsupported", "this runtime has no Web Crypto (crypto.subtle) to check an Ed25519 signature with");
|
|
512
|
+
let imported;
|
|
513
|
+
try {
|
|
514
|
+
imported = await subtle.importKey("raw", key, { name: "Ed25519" }, false, ["verify"]);
|
|
515
|
+
} catch (e) {
|
|
516
|
+
throw new VerifyError("unsupported", `this runtime's Web Crypto cannot check an Ed25519 signature: ${e instanceof Error ? e.message : String(e)}`);
|
|
517
|
+
}
|
|
518
|
+
return subtle.verify({ name: "Ed25519" }, imported, signature, data);
|
|
519
|
+
}
|
|
520
|
+
var isText = (v) => typeof v === "string" && v !== "";
|
|
521
|
+
async function verifyAnswer(jws, options = {}) {
|
|
522
|
+
if (!isObject(options)) throw new VerifyError("bad_options", "options must be an object: keys or jwksUrl, and expect");
|
|
523
|
+
const o = options;
|
|
524
|
+
const tolerance = o.clockToleranceSeconds ?? 0;
|
|
525
|
+
if (typeof tolerance !== "number" || !Number.isSafeInteger(tolerance) || tolerance < 0) {
|
|
526
|
+
throw new VerifyError("bad_options", "options.clockToleranceSeconds must be a whole number of seconds, 0 or more");
|
|
527
|
+
}
|
|
528
|
+
const now = o.now ?? Math.floor(Date.now() / 1e3);
|
|
529
|
+
if (typeof now !== "number" || !Number.isFinite(now)) throw new VerifyError("bad_options", "options.now must be a time in Unix seconds");
|
|
530
|
+
const issuer = o.issuer ?? ANSWER_ISSUER;
|
|
531
|
+
if (!isText(issuer)) throw new VerifyError("bad_options", "options.issuer must be a string");
|
|
532
|
+
const expect = parseAnswerExpectation(o.expect);
|
|
533
|
+
if (o.keys !== void 0 && o.jwksUrl !== void 0) throw new VerifyError("bad_options", "pass keys or jwksUrl, one of the two");
|
|
534
|
+
if (o.jwksUrl !== void 0 && !isText(o.jwksUrl)) throw new VerifyError("bad_options", "options.jwksUrl must be a URL");
|
|
535
|
+
const parts = typeof jws === "string" ? jws.trim().split(".") : [];
|
|
536
|
+
if (parts.length !== 3) throw new VerifyError("bad_answer", "not a compact JWS: three base64url parts joined by dots");
|
|
537
|
+
const headerBytes = fromBase64Url(parts[0]);
|
|
538
|
+
const payloadBytes = fromBase64Url(parts[1]);
|
|
539
|
+
const signature = fromBase64Url(parts[2]);
|
|
540
|
+
const header = headerBytes && jsonObject(headerBytes);
|
|
541
|
+
if (!header || !payloadBytes || !signature) throw new VerifyError("bad_answer", "not a compact JWS: a part is not base64url, or the header is not JSON");
|
|
542
|
+
if (header.alg !== "EdDSA") throw new VerifyError("bad_answer", `the answer is signed with ${JSON.stringify(header.alg)}, not EdDSA`);
|
|
543
|
+
if (signature.length !== 64) throw new VerifyError("bad_answer", "the signature is not an Ed25519 signature");
|
|
544
|
+
const keys = o.keys !== void 0 ? passedKeys(o.keys) : await fetchKeys(o.jwksUrl ?? SIGNING_KEYS_URL, o);
|
|
545
|
+
const key = keys.find((k) => k.kid === header.kid);
|
|
546
|
+
if (!key) throw new VerifyError("unknown_key", `the answer is signed by key ${JSON.stringify(header.kid)}, which is not one of the keys`);
|
|
547
|
+
if (!await ed25519Verify(key.raw, signature, new TextEncoder().encode(`${parts[0]}.${parts[1]}`))) {
|
|
548
|
+
throw new VerifyError("bad_signature", "the signature does not match: the answer was changed, or another key signed it");
|
|
549
|
+
}
|
|
550
|
+
const c = jsonObject(payloadBytes);
|
|
551
|
+
if (!c) throw new VerifyError("bad_answer", "the signed payload is not a JSON object");
|
|
552
|
+
if (c.typ !== ANSWER_TYPE) throw new VerifyError("bad_answer", `not a signed answer: typ is ${JSON.stringify(c.typ)}, not "${ANSWER_TYPE}"`);
|
|
553
|
+
if (c.iss !== issuer) throw new VerifyError("bad_answer", `the answer is from ${JSON.stringify(c.iss)}, not ${issuer}`);
|
|
554
|
+
if (!Number.isSafeInteger(c.iat) || !Number.isSafeInteger(c.exp)) throw new VerifyError("bad_answer", "the answer has no iat or no exp");
|
|
555
|
+
for (const name of ["kind", "chainId", "chain", "symbol", "status"]) {
|
|
556
|
+
if (!isText(c[name])) throw new VerifyError("bad_answer", `the answer has no ${name}`);
|
|
557
|
+
}
|
|
558
|
+
if (c.status === "registered" && (!isText(c.tokenAddress) || !isText(c.holder) || !Number.isSafeInteger(c.crownId))) {
|
|
559
|
+
throw new VerifyError("bad_answer", "a registered answer must name the token, the registration and its holder");
|
|
560
|
+
}
|
|
561
|
+
if (now >= c.exp + tolerance) throw new VerifyError("expired", `the answer expired at ${c.exp} (Unix seconds): fetch a new one`);
|
|
562
|
+
const claims = c;
|
|
563
|
+
if (expect) {
|
|
564
|
+
if (expect.chain !== void 0 && claims.chain !== expect.chain) throw new VerifyError("expectation_mismatch", `the answer is about chain ${claims.chain}, not ${expect.chain}`);
|
|
565
|
+
if (expect.chainId !== void 0 && claims.chainId !== expect.chainId) throw new VerifyError("expectation_mismatch", `the answer is about chain ${claims.chainId}, not ${expect.chainId}`);
|
|
566
|
+
if (expect.symbol !== void 0 && claims.symbol.toUpperCase() !== expect.symbol.toUpperCase()) {
|
|
567
|
+
throw new VerifyError("expectation_mismatch", `the answer is about ${claims.symbol}, not ${expect.symbol}`);
|
|
568
|
+
}
|
|
569
|
+
if (expect.kind !== void 0 && claims.kind !== expect.kind) throw new VerifyError("expectation_mismatch", `the answer is a ${claims.kind}, not a ${expect.kind}`);
|
|
570
|
+
}
|
|
571
|
+
return claims;
|
|
572
|
+
}
|
|
333
573
|
export {
|
|
574
|
+
ANSWER_ISSUER,
|
|
575
|
+
ANSWER_TYPE,
|
|
334
576
|
CHECKOUTS,
|
|
335
577
|
MIN_CONFIRMATIONS,
|
|
336
578
|
NATIVE_TOKEN,
|
|
579
|
+
SIGNING_KEYS_RECORD,
|
|
580
|
+
SIGNING_KEYS_URL,
|
|
337
581
|
VerifyError,
|
|
338
582
|
encodeHashOrder,
|
|
339
583
|
encodePaidBy,
|
|
340
584
|
isKnownCheckout,
|
|
585
|
+
signingKeysFromRecord,
|
|
586
|
+
verifyAnswer,
|
|
341
587
|
verifyPayment
|
|
342
588
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaindaddy/js",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Chain Daddy for any website: Sign in with Chain Daddy (a popup and a button), a coin's apps on its own site (<cd-app>), checkout for a coin's store in a popup, and a server-side check that a store order was paid on chain",
|
|
5
5
|
"homepage": "https://docs.chaindaddy.io/developer/javascript-sdk",
|
|
6
6
|
"repository": {
|