From 65f87fa613f074888c75e9c89c5be775c1138c0d Mon Sep 17 00:00:00 2001 From: Support Bot Date: Fri, 24 Jul 2026 00:36:20 +0000 Subject: [PATCH] fix(sdk-coin-icp): parse certificate to detect canister rejections The ICP /api/v3/canister/call endpoint returns HTTP 200 with outer status='replied' even when the canister rejects the transaction (e.g. insufficient funds). The rejection details are encoded in the certificate's request_status subtree. Previously, broadcastTransaction only checked the outer status field and treated any 'replied' response as a success. This caused silent failures: withdrawals that were rejected by the ICP ledger (e.g. due to insufficient balance) were reported as successful by the SDK. This fix adds ICP labeled hash tree traversal to extract the actual request status from the certificate. When the certificate contains request_status.status='rejected', the broadcast now throws with the canister's reject_message, surfacing the real failure to callers. Changes: - Add IcpCertificate, IcpHashTree types and REQUEST_STATUS_* constants to iface.ts - Add lookupPathInTree, extractRequestStatusFromCertificate, and helper tree-traversal methods to Utils class - Update broadcastTransaction to decode the certificate and throw on canister rejection - Replace the test broadcast response fixture with a minimal success response; rename the original rejected fixture to PublicNodeApiBroadcastResponseRejected - Add test case that verifies canister rejections are surfaced Ticket: COINS-1257 Session-Id: c40a0622-f3d1-48cc-82a2-5d3debf68691 Task-Id: b2e68cd6-bb53-4c2c-b0b6-f8c4e7f3d652 --- modules/sdk-coin-icp/src/icp.ts | 17 ++- modules/sdk-coin-icp/src/lib/iface.ts | 24 ++- modules/sdk-coin-icp/src/lib/utils.ts | 139 ++++++++++++++++++ modules/sdk-coin-icp/test/resources/icp.ts | 9 +- .../transactionBuilder/transactionRecover.ts | 7 + yarn.lock | 92 +++++------- 6 files changed, 230 insertions(+), 58 deletions(-) diff --git a/modules/sdk-coin-icp/src/icp.ts b/modules/sdk-coin-icp/src/icp.ts index 75b8303d62..2d6cf6dd70 100644 --- a/modules/sdk-coin-icp/src/icp.ts +++ b/modules/sdk-coin-icp/src/icp.ts @@ -31,6 +31,7 @@ import * as mpc from '@bitgo/sdk-lib-mpc'; import { CurveType, + IcpCertificate, LEDGER_CANISTER_ID, TESTNET_LEDGER_CANISTER_ID, PayloadsData, @@ -38,6 +39,8 @@ import { PublicNodeSubmitResponse, RecoveryOptions, RecoveryTransaction, + REQUEST_STATUS_REJECTED, + REQUEST_STATUS_REPLIED, ROOT_PATH, Signatures, SigningPayload, @@ -284,8 +287,18 @@ export class Icp extends BaseCoin { const decodedResponse = utils.cborDecode(response.data) as PublicNodeSubmitResponse; - if (decodedResponse.status === 'replied') { - // it is considered a success because ICP returns response in a CBOR map with a status of 'replied' + if (decodedResponse.status === REQUEST_STATUS_REPLIED) { + // The outer status 'replied' means the IC accepted and processed the HTTP call. + // The certificate contains the actual canister execution result. A canister can + // still reject the call (e.g. insufficient funds) even when the outer status is + // 'replied', so we must inspect the certificate's request_status subtree. + if (decodedResponse.certificate) { + const certificate = utils.cborDecode(Buffer.from(decodedResponse.certificate)) as IcpCertificate; + const { requestStatus, rejectMessage } = utils.extractRequestStatusFromCertificate(certificate); + if (requestStatus === REQUEST_STATUS_REJECTED) { + throw new Error(`Transaction rejected by canister: ${rejectMessage ?? 'unknown reason'}`); + } + } return {}; // returned empty object as ICP does not return a txid } else { throw new Error(`Unexpected response status from node: ${decodedResponse.status}`); diff --git a/modules/sdk-coin-icp/src/lib/iface.ts b/modules/sdk-coin-icp/src/lib/iface.ts index f1a83621d6..e3440d271c 100644 --- a/modules/sdk-coin-icp/src/lib/iface.ts +++ b/modules/sdk-coin-icp/src/lib/iface.ts @@ -4,6 +4,9 @@ import { TssVerifyAddressOptions, } from '@bitgo/sdk-core'; +export const REQUEST_STATUS_REPLIED = 'replied'; +export const REQUEST_STATUS_REJECTED = 'rejected'; + export const MAX_INGRESS_TTL = 5 * 60 * 1000_000_000; // 5 minutes in nanoseconds export const PERMITTED_DRIFT = 60 * 1000_000_000; // 60 seconds in nanoseconds export const LEDGER_CANISTER_ID = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2, 1, 1]); // Uint8Array value for "00000000000000020101" and the string value is "ryjl3-tyaaa-aaaaa-aaaba-cai" @@ -207,7 +210,26 @@ export interface UnsignedSweepRecoveryTransaction { export interface PublicNodeSubmitResponse { status: string; -} + certificate?: Uint8Array; +} + +export interface IcpCertificate { + tree: IcpHashTree; + signature: Uint8Array; +} + +// ICP labeled hash tree node types per the IC HTTP spec: +// [0] = Empty +// [1, left, right] = Fork +// [2, label, subtree] = Labeled +// [3, value] = Leaf +// [4, hash] = Pruned +export type IcpHashTree = + | [0] + | [1, IcpHashTree, IcpHashTree] + | [2, Uint8Array, IcpHashTree] + | [3, Uint8Array] + | [4, Uint8Array]; export interface AccountIdentifierHash { hash: Buffer; diff --git a/modules/sdk-coin-icp/src/lib/utils.ts b/modules/sdk-coin-icp/src/lib/utils.ts index 3cb81f0dc8..765a3fdd25 100644 --- a/modules/sdk-coin-icp/src/lib/utils.ts +++ b/modules/sdk-coin-icp/src/lib/utils.ts @@ -21,6 +21,8 @@ import { CurveType, AccountIdentifierHash, CborUnsignedTransaction, + IcpCertificate, + IcpHashTree, MAX_INGRESS_TTL, } from './iface'; import { KeyPair as IcpKeyPair } from './keyPair'; @@ -774,6 +776,143 @@ export class Utils implements BaseUtils { } throw new Error('Invalid accountHash format'); } + + /** + * Looks up a path in an ICP labeled hash tree. + * + * The ICP state tree uses a specific CBOR array encoding: + * [0] = Empty + * [1, l, r] = Fork + * [2, label, t] = Labeled + * [3, value] = Leaf + * [4, hash] = Pruned + * + * @param tree The root node of the ICP labeled hash tree. + * @param path Ordered path segments (as Buffers or strings). + * @returns The leaf value as a Buffer, or undefined if not found. + */ + lookupPathInTree(tree: IcpHashTree, path: (Buffer | string)[]): Buffer | undefined { + if (path.length === 0) { + if (tree[0] === 3) { + return Buffer.from(tree[1] as Uint8Array); + } + return undefined; + } + + const [head, ...rest] = path; + const labelBytes = typeof head === 'string' ? Buffer.from(head, 'utf8') : head; + + return this.findLabeledSubtree(tree, labelBytes, rest); + } + + private findLabeledSubtree(tree: IcpHashTree, label: Buffer, remainingPath: (Buffer | string)[]): Buffer | undefined { + const nodeType = tree[0]; + + if (nodeType === 1) { + // Fork: search both sides + const left = this.findLabeledSubtree(tree[1] as IcpHashTree, label, remainingPath); + if (left !== undefined) return left; + return this.findLabeledSubtree(tree[2] as IcpHashTree, label, remainingPath); + } + + if (nodeType === 2) { + // Labeled: check if this label matches + const nodeLabel = Buffer.from(tree[1] as Uint8Array); + if (nodeLabel.equals(label)) { + return this.lookupPathInTree(tree[2] as IcpHashTree, remainingPath); + } + } + + return undefined; + } + + /** + * Extracts the request status from an ICP v3 API call response certificate. + * + * For a `/api/v3/canister/call` response where the outer status is 'replied', + * the certificate tree contains the actual request outcome at the path + * ["request_status", , "status"]. + * + * @param certificate The decoded ICP certificate object. + * @returns Object containing the request status and optional reject message. + */ + extractRequestStatusFromCertificate(certificate: IcpCertificate): { + requestStatus: string; + rejectMessage?: string; + rejectCode?: number; + } { + const tree = certificate.tree; + + // The status node is nested under ["request_status", , "status"]. + // We don't know the request_id in advance, so we search the tree for any "status" + // leaf inside the "request_status" subtree, skipping the intermediate request_id level. + const statusFromSubtree = this.searchForLeafInSubtree(tree, 'request_status', 'status'); + if (statusFromSubtree === undefined) { + // No request_status/status found — treat as unknown (not a rejection) + return { requestStatus: 'unknown' }; + } + + const requestStatus = statusFromSubtree.toString('utf8'); + + if (requestStatus === 'rejected') { + const rejectMsg = this.searchForLeafInSubtree(tree, 'request_status', 'reject_message'); + const rejectCodeBuf = this.searchForLeafInSubtree(tree, 'request_status', 'reject_code'); + return { + requestStatus, + rejectMessage: rejectMsg?.toString('utf8'), + rejectCode: rejectCodeBuf ? rejectCodeBuf.readUIntBE(0, rejectCodeBuf.length) : undefined, + }; + } + + return { requestStatus }; + } + + /** + * Searches for a labeled leaf nested inside a named outer path segment. + * Used to traverse the request_status subtree without knowing the intermediate + * request_id hash key. + */ + private searchForLeafInSubtree(tree: IcpHashTree, outerLabel: string, innerLabel: string): Buffer | undefined { + const outer = Buffer.from(outerLabel, 'utf8'); + const inner = Buffer.from(innerLabel, 'utf8'); + return this.findNestedLabel(tree, outer, inner, false); + } + + private findNestedLabel( + tree: IcpHashTree, + outerLabel: Buffer, + innerLabel: Buffer, + insideOuter: boolean + ): Buffer | undefined { + const nodeType = tree[0]; + + if (nodeType === 1) { + // Fork + const left = this.findNestedLabel(tree[1] as IcpHashTree, outerLabel, innerLabel, insideOuter); + if (left !== undefined) return left; + return this.findNestedLabel(tree[2] as IcpHashTree, outerLabel, innerLabel, insideOuter); + } + + if (nodeType === 2) { + const nodeLabel = Buffer.from(tree[1] as Uint8Array); + if (!insideOuter && nodeLabel.equals(outerLabel)) { + // Entered the outer subtree — now search for innerLabel anywhere inside + return this.findNestedLabel(tree[2] as IcpHashTree, outerLabel, innerLabel, true); + } + if (insideOuter && nodeLabel.equals(innerLabel)) { + // Found the inner label inside the outer subtree + const subtree = tree[2] as IcpHashTree; + if (subtree[0] === 3) { + return Buffer.from(subtree[1] as Uint8Array); + } + return undefined; + } + // Continue searching inside same subtree + return this.findNestedLabel(tree[2] as IcpHashTree, outerLabel, innerLabel, insideOuter); + } + + return undefined; + } } const utils = new Utils(); diff --git a/modules/sdk-coin-icp/test/resources/icp.ts b/modules/sdk-coin-icp/test/resources/icp.ts index 554668cd95..48ebf4c313 100644 --- a/modules/sdk-coin-icp/test/resources/icp.ts +++ b/modules/sdk-coin-icp/test/resources/icp.ts @@ -376,9 +376,16 @@ export const TxnIdWithDefaultMemo = 'a8c20ea0d9c984400f01b7f9d5325773862751c3277 export const TxnIdWithMemo = 'ac2157cdca60e8c54c127434893dc6f65548aefa0603d3f3232f01ec8f2e0c82'; -export const PublicNodeApiBroadcastResponse = +// CBOR-encoded ICP v3 /call response where the outer status is 'replied' but the +// certificate's request_status tree shows 'rejected' (e.g. insufficient funds). +// Used to verify that broadcastTransaction correctly surfaces canister rejections. +export const PublicNodeApiBroadcastResponseRejected = 'd9d9f7a266737461747573677265706c6965646b6365727469666963617465590317d9d9f7a2647472656583018301820458200548bee8d4a0ada144f09841a1814e0c7b06a9c525e19d07990c27a8d5dfebe983018204582059964f7dcd455bb5333f0424188250c1bafdfee505b6d4bffce6b93d84b02fbc83024e726571756573745f7374617475738301820458208ec4679d2b1049ab4154b9113c88a7bacc1772831a799d0337e650fe093339648302582086ab628166c7c823c4fac064f42bcb6c6939aec9a63ce88a2eef553475fd55958301830183024a6572726f725f636f646582034649433035303383024b72656a6563745f636f646582034105830183024e72656a6563745f6d65737361676582035901984572726f722066726f6d2043616e69737465722072796a6c332d74796161612d61616161612d61616162612d6361693a2043616e69737465722063616c6c656420606963302e74726170602077697468206d6573736167653a20746865206465626974206163636f756e7420646f65736e2774206861766520656e6f7567682066756e647320746f20636f6d706c65746520746865207472616e73616374696f6e2c2063757272656e742062616c616e63653a20302e303030303030303020546f6b656e2e0a436f6e736964657220677261636566756c6c792068616e646c696e67206661696c757265732066726f6d20746869732063616e6973746572206f7220616c746572696e67207468652063616e697374657220746f2068616e646c6520657863657074696f6e732e2053656520646f63756d656e746174696f6e3a20687474703a2f2f696e7465726e6574636f6d70757465722e6f72672f646f63732f63757272656e742f7265666572656e6365732f657865637574696f6e2d6572726f727323747261707065642d6578706c696369746c7983024673746174757382034872656a6563746564830182045820535b0b886d8f16a1d4909618e38af89fa407ef6f606c1e667ab31015f35db57f83024474696d658203498ad7d790e682f69918697369676e61747572655830a26f5ae0c1396f6005487223031062886416c7d4ea37a1b2a7b64a287b376a5f4495cffd99a69bf4f4fd95e53794f415'; +// CBOR-encoded ICP v3 /call success response: status='replied' with no certificate field. +// Represents a synchronous canister call that was accepted and completed successfully. +export const PublicNodeApiBroadcastResponse = 'd9d9f7a166737461747573677265706c696564'; + export const RecoverySignedTransactionWithDefaultMemo = 'b9000367636f6e74656e74b900066c726571756573745f747970656463616c6c6b63616e69737465725f69644a000000000000000201016b6d6574686f645f6e616d656773656e645f70626361726758430a02080012080a0608f0c5eadc031a0308904e2a220a20c3d30f404955975adaba89f2e1ebc75c1f44a6a204578afce8f3780d64fe252e3a0a0880a48596eb92b599186673656e646572581db32b534dcc87771406ce0d93595733ca9c635f0f2f3e29559e8806a1026e696e67726573735f6578706972791b1832d4ce93deb2006d73656e6465725f7075626b6579d84058583056301006072a8648ce3d020106052b8104000a0342000482ef63ee315d6323c616970c93267ca92ee81a691027f9c856ccd23f414f32c5209b7b6f64f42e66c9c5adf064cf0f372ae1b27c9a0fcbc4dcaa8fb5cd5ba0e66a73656e6465725f7369675840cd8f6cde4f72597767b740c3eae6bc8374afa73fd099a474c7d02e60dee5b3f3239829c223f0aec28ca6fe28d735068b7ab77bef6ef89f0a7307e9fdb5f15d09'; diff --git a/modules/sdk-coin-icp/test/unit/transactionBuilder/transactionRecover.ts b/modules/sdk-coin-icp/test/unit/transactionBuilder/transactionRecover.ts index e0ede7f526..babd9f162b 100644 --- a/modules/sdk-coin-icp/test/unit/transactionBuilder/transactionRecover.ts +++ b/modules/sdk-coin-icp/test/unit/transactionBuilder/transactionRecover.ts @@ -148,6 +148,13 @@ describe('ICP transaction recovery', async () => { ); }); + it('should fail to recover if canister rejects the transaction (e.g. insufficient funds)', async () => { + nock.cleanAll(); + const rejectedResponse = Buffer.from(testData.PublicNodeApiBroadcastResponseRejected, 'hex'); + nock(nodeUrl).post(broadcastEndpoint).reply(200, rejectedResponse); + await icp.recover(recoveryParams).should.rejectedWith(/Transaction rejected by canister/); + }); + it('should fail to recover txn if balance is low', async () => { sinon.restore(); setupLowBalanceStubs(); diff --git a/yarn.lock b/yarn.lock index e78d7ef1f2..536d661695 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8055,23 +8055,23 @@ bn.js@^5.1.1, bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz#16a9e409616b23fef3ccbedb8d42f13bff80295e" integrity sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w== -body-parser@1.20.3, body-parser@1.20.6, body-parser@^1.19.0, body-parser@^1.20.3: - version "1.20.6" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz#60c789c78e0992d906da0a29d71ae01d15c1ed76" - integrity sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g== +body-parser@1.20.3, body-parser@^1.19.0, body-parser@^1.20.3: + version "1.20.3" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: - bytes "~3.1.2" + bytes "3.1.2" content-type "~1.0.5" debug "2.6.9" depd "2.0.0" - destroy "~1.2.0" - http-errors "~2.0.1" - iconv-lite "~0.4.24" - on-finished "~2.4.1" - qs "~6.15.1" - raw-body "~2.5.3" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" type-is "~1.6.18" - unpipe "~1.0.0" + unpipe "1.0.0" bonjour-service@^1.2.1: version "1.3.0" @@ -8411,7 +8411,7 @@ bytebuffer@5.0.1: dependencies: long "~3" -bytes@3.1.2, bytes@~3.1.2: +bytes@3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== @@ -10097,7 +10097,7 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -destroy@1.2.0, destroy@~1.2.0: +destroy@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== @@ -12790,17 +12790,6 @@ http-errors@~1.6.1, http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" -http-errors@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" - integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== - dependencies: - depd "~2.0.0" - inherits "~2.0.4" - setprototypeof "~1.2.0" - statuses "~2.0.2" - toidentifier "~1.0.1" - http-parser-js@>=0.5.1: version "0.5.10" resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz" @@ -12929,6 +12918,13 @@ ic0@^0.3.2: "@dfinity/principal" "^2.1.3" cross-fetch "^3.1.5" +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + iconv-lite@^0.6.2: version "0.6.3" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" @@ -12943,13 +12939,6 @@ iconv-lite@^0.7.0, iconv-lite@^0.7.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -iconv-lite@~0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" @@ -17477,10 +17466,10 @@ propagate@^2.0.0: resolved "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz" integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== -protobufjs@7.2.5, protobufjs@7.6.5, protobufjs@^6.8.8, protobufjs@^7.5.5, protobufjs@^7.5.8, protobufjs@~6.11.2, protobufjs@~6.11.3: - version "7.6.5" - resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz#7b9250cdaf4a06139a9f0fe468a40d7d4febca71" - integrity sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw== +protobufjs@7.2.5, protobufjs@7.6.4, protobufjs@^6.8.8, protobufjs@^7.5.5, protobufjs@^7.5.8, protobufjs@~6.11.2, protobufjs@~6.11.3: + version "7.6.4" + resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz#8bb000300026efd63eb7951d26e5dbb38f5658f2" + integrity sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -17654,7 +17643,7 @@ qrcode@^1.5.1: pngjs "^5.0.0" yargs "^15.3.1" -qs@6.13.0, qs@6.14.0, qs@6.15.2, qs@^6.11.0, qs@^6.11.2, qs@^6.12.3, qs@^6.5.1, qs@~6.15.1: +qs@6.13.0, qs@6.14.0, qs@6.15.2, qs@^6.11.0, qs@^6.11.2, qs@^6.12.3, qs@^6.5.1: version "6.15.2" resolved "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz#fd55426d710403ddccc45e0f9eab16db7727ece9" integrity sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw== @@ -17725,15 +17714,15 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@~2.5.3: - version "2.5.3" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2" - integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA== +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: - bytes "~3.1.2" - http-errors "~2.0.1" - iconv-lite "~0.4.24" - unpipe "~1.0.0" + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" react-base16-styling@^0.6.0: version "0.6.0" @@ -18685,7 +18674,7 @@ setprototypeof@1.1.1: resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -setprototypeof@1.2.0, setprototypeof@~1.2.0: +setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== @@ -19275,11 +19264,6 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -statuses@~2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" - integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== - stellar-base@^8.2.2: version "8.2.2" resolved "https://registry.npmjs.org/stellar-base/-/stellar-base-8.2.2.tgz" @@ -20069,7 +20053,7 @@ toidentifier@1.0.0: resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -toidentifier@1.0.1, toidentifier@~1.0.1: +toidentifier@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== @@ -20586,9 +20570,9 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@~1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== untildify@^4.0.0: