@ledgerhq/coin-stellar 6.26.0 → 6.26.2
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/.oxlintrc.json +7 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +194 -0
- package/knip.json +12 -0
- package/lib/api/index.d.ts +5 -5
- package/lib/api/index.d.ts.map +1 -1
- package/lib/api/index.js +12 -10
- package/lib/api/index.js.map +1 -1
- package/lib/config.d.ts +1 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +2 -0
- package/lib/config.js.map +1 -1
- package/lib/constants.d.ts.map +1 -1
- package/lib/constants.js +3 -1
- package/lib/constants.js.map +1 -1
- package/lib/logic/broadcast.d.ts.map +1 -1
- package/lib/logic/broadcast.js +2 -0
- package/lib/logic/broadcast.js.map +1 -1
- package/lib/logic/combine.d.ts +1 -1
- package/lib/logic/combine.d.ts.map +1 -1
- package/lib/logic/combine.js +2 -0
- package/lib/logic/combine.js.map +1 -1
- package/lib/logic/common.d.ts +1 -1
- package/lib/logic/common.d.ts.map +1 -1
- package/lib/logic/common.js +4 -2
- package/lib/logic/common.js.map +1 -1
- package/lib/logic/craftTransaction.d.ts +1 -1
- package/lib/logic/craftTransaction.d.ts.map +1 -1
- package/lib/logic/craftTransaction.js +10 -8
- package/lib/logic/craftTransaction.js.map +1 -1
- package/lib/logic/estimateFees.d.ts.map +1 -1
- package/lib/logic/estimateFees.js +2 -0
- package/lib/logic/estimateFees.js.map +1 -1
- package/lib/logic/getBalance.d.ts +1 -1
- package/lib/logic/getBalance.d.ts.map +1 -1
- package/lib/logic/getBalance.js +4 -2
- package/lib/logic/getBalance.js.map +1 -1
- package/lib/logic/getBlock.d.ts +1 -1
- package/lib/logic/getBlock.d.ts.map +1 -1
- package/lib/logic/getBlock.js +39 -34
- package/lib/logic/getBlock.js.map +1 -1
- package/lib/logic/getBlockInfo.d.ts +1 -1
- package/lib/logic/getBlockInfo.d.ts.map +1 -1
- package/lib/logic/getBlockInfo.js +2 -0
- package/lib/logic/getBlockInfo.js.map +1 -1
- package/lib/logic/index.d.ts +12 -12
- package/lib/logic/index.d.ts.map +1 -1
- package/lib/logic/index.js +2 -0
- package/lib/logic/index.js.map +1 -1
- package/lib/logic/lastBlock.d.ts +1 -1
- package/lib/logic/lastBlock.d.ts.map +1 -1
- package/lib/logic/lastBlock.js +2 -0
- package/lib/logic/lastBlock.js.map +1 -1
- package/lib/logic/listOperations.d.ts +1 -1
- package/lib/logic/listOperations.d.ts.map +1 -1
- package/lib/logic/listOperations.js +6 -4
- package/lib/logic/listOperations.js.map +1 -1
- package/lib/logic/sdkWrapper.d.ts +1 -1
- package/lib/logic/sdkWrapper.d.ts.map +1 -1
- package/lib/logic/sdkWrapper.js +2 -0
- package/lib/logic/sdkWrapper.js.map +1 -1
- package/lib/logic/utils.d.ts.map +1 -1
- package/lib/logic/utils.js +4 -2
- package/lib/logic/utils.js.map +1 -1
- package/lib/logic/validateAddress.d.ts +1 -1
- package/lib/logic/validateAddress.d.ts.map +1 -1
- package/lib/logic/validateAddress.js +2 -0
- package/lib/logic/validateAddress.js.map +1 -1
- package/lib/logic/validateIntent.d.ts +2 -2
- package/lib/logic/validateIntent.d.ts.map +1 -1
- package/lib/logic/validateIntent.js +21 -19
- package/lib/logic/validateIntent.js.map +1 -1
- package/lib/logic/validateMemo.d.ts +1 -1
- package/lib/logic/validateMemo.d.ts.map +1 -1
- package/lib/logic/validateMemo.js +7 -5
- package/lib/logic/validateMemo.js.map +1 -1
- package/lib/network/horizon.d.ts +6 -7
- package/lib/network/horizon.d.ts.map +1 -1
- package/lib/network/horizon.js +48 -95
- package/lib/network/horizon.js.map +1 -1
- package/lib/network/horizonErrorBody.d.ts +29 -0
- package/lib/network/horizonErrorBody.d.ts.map +1 -0
- package/lib/network/horizonErrorBody.js +139 -0
- package/lib/network/horizonErrorBody.js.map +1 -0
- package/lib/network/horizonLedgerErrors.d.ts.map +1 -1
- package/lib/network/horizonLedgerErrors.js +28 -4
- package/lib/network/horizonLedgerErrors.js.map +1 -1
- package/lib/network/index.d.ts +1 -1
- package/lib/network/index.d.ts.map +1 -1
- package/lib/network/index.js +2 -0
- package/lib/network/index.js.map +1 -1
- package/lib/network/serialization.d.ts +6 -4
- package/lib/network/serialization.d.ts.map +1 -1
- package/lib/network/serialization.js +95 -91
- package/lib/network/serialization.js.map +1 -1
- package/lib/polyfill.d.ts.map +1 -1
- package/lib/polyfill.js +6 -4
- package/lib/polyfill.js.map +1 -1
- package/lib/supportedFeatures.d.ts +1 -1
- package/lib/supportedFeatures.d.ts.map +1 -1
- package/lib/supportedFeatures.js +3 -1
- package/lib/supportedFeatures.js.map +1 -1
- package/lib/types/errors.d.ts.map +1 -1
- package/lib/types/errors.js +14 -12
- package/lib/types/errors.js.map +1 -1
- package/lib/types/index.d.ts +2 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +2 -0
- package/lib/types/index.js.map +1 -1
- package/lib/types/model.d.ts +6 -6
- package/lib/types/model.d.ts.map +1 -1
- package/lib/types/model.js +7 -5
- package/lib/types/model.js.map +1 -1
- package/lib-es/api/index.d.ts +5 -5
- package/lib-es/api/index.d.ts.map +1 -1
- package/lib-es/api/index.js +20 -18
- package/lib-es/api/index.js.map +1 -1
- package/lib-es/config.d.ts +1 -1
- package/lib-es/config.d.ts.map +1 -1
- package/lib-es/config.js +3 -1
- package/lib-es/config.js.map +1 -1
- package/lib-es/constants.d.ts.map +1 -1
- package/lib-es/constants.js +3 -1
- package/lib-es/constants.js.map +1 -1
- package/lib-es/logic/broadcast.d.ts.map +1 -1
- package/lib-es/logic/broadcast.js +3 -1
- package/lib-es/logic/broadcast.js.map +1 -1
- package/lib-es/logic/combine.d.ts +1 -1
- package/lib-es/logic/combine.d.ts.map +1 -1
- package/lib-es/logic/combine.js +3 -1
- package/lib-es/logic/combine.js.map +1 -1
- package/lib-es/logic/common.d.ts +1 -1
- package/lib-es/logic/common.d.ts.map +1 -1
- package/lib-es/logic/common.js +5 -3
- package/lib-es/logic/common.js.map +1 -1
- package/lib-es/logic/craftTransaction.d.ts +1 -1
- package/lib-es/logic/craftTransaction.d.ts.map +1 -1
- package/lib-es/logic/craftTransaction.js +15 -13
- package/lib-es/logic/craftTransaction.js.map +1 -1
- package/lib-es/logic/estimateFees.d.ts.map +1 -1
- package/lib-es/logic/estimateFees.js +3 -1
- package/lib-es/logic/estimateFees.js.map +1 -1
- package/lib-es/logic/getBalance.d.ts +1 -1
- package/lib-es/logic/getBalance.d.ts.map +1 -1
- package/lib-es/logic/getBalance.js +5 -3
- package/lib-es/logic/getBalance.js.map +1 -1
- package/lib-es/logic/getBlock.d.ts +1 -1
- package/lib-es/logic/getBlock.d.ts.map +1 -1
- package/lib-es/logic/getBlock.js +42 -37
- package/lib-es/logic/getBlock.js.map +1 -1
- package/lib-es/logic/getBlockInfo.d.ts +1 -1
- package/lib-es/logic/getBlockInfo.d.ts.map +1 -1
- package/lib-es/logic/getBlockInfo.js +3 -1
- package/lib-es/logic/getBlockInfo.js.map +1 -1
- package/lib-es/logic/index.d.ts +12 -12
- package/lib-es/logic/index.d.ts.map +1 -1
- package/lib-es/logic/index.js +14 -12
- package/lib-es/logic/index.js.map +1 -1
- package/lib-es/logic/lastBlock.d.ts +1 -1
- package/lib-es/logic/lastBlock.d.ts.map +1 -1
- package/lib-es/logic/lastBlock.js +3 -1
- package/lib-es/logic/lastBlock.js.map +1 -1
- package/lib-es/logic/listOperations.d.ts +1 -1
- package/lib-es/logic/listOperations.d.ts.map +1 -1
- package/lib-es/logic/listOperations.js +7 -5
- package/lib-es/logic/listOperations.js.map +1 -1
- package/lib-es/logic/sdkWrapper.d.ts +1 -1
- package/lib-es/logic/sdkWrapper.d.ts.map +1 -1
- package/lib-es/logic/sdkWrapper.js +4 -2
- package/lib-es/logic/sdkWrapper.js.map +1 -1
- package/lib-es/logic/utils.d.ts.map +1 -1
- package/lib-es/logic/utils.js +6 -4
- package/lib-es/logic/utils.js.map +1 -1
- package/lib-es/logic/validateAddress.d.ts +1 -1
- package/lib-es/logic/validateAddress.d.ts.map +1 -1
- package/lib-es/logic/validateAddress.js +3 -1
- package/lib-es/logic/validateAddress.js.map +1 -1
- package/lib-es/logic/validateIntent.d.ts +2 -2
- package/lib-es/logic/validateIntent.d.ts.map +1 -1
- package/lib-es/logic/validateIntent.js +30 -28
- package/lib-es/logic/validateIntent.js.map +1 -1
- package/lib-es/logic/validateMemo.d.ts +1 -1
- package/lib-es/logic/validateMemo.d.ts.map +1 -1
- package/lib-es/logic/validateMemo.js +8 -6
- package/lib-es/logic/validateMemo.js.map +1 -1
- package/lib-es/network/horizon.d.ts +6 -7
- package/lib-es/network/horizon.d.ts.map +1 -1
- package/lib-es/network/horizon.js +57 -104
- package/lib-es/network/horizon.js.map +1 -1
- package/lib-es/network/horizonErrorBody.d.ts +29 -0
- package/lib-es/network/horizonErrorBody.d.ts.map +1 -0
- package/lib-es/network/horizonErrorBody.js +134 -0
- package/lib-es/network/horizonErrorBody.js.map +1 -0
- package/lib-es/network/horizonLedgerErrors.d.ts.map +1 -1
- package/lib-es/network/horizonLedgerErrors.js +30 -6
- package/lib-es/network/horizonLedgerErrors.js.map +1 -1
- package/lib-es/network/index.d.ts +1 -1
- package/lib-es/network/index.d.ts.map +1 -1
- package/lib-es/network/index.js +3 -1
- package/lib-es/network/index.js.map +1 -1
- package/lib-es/network/serialization.d.ts +6 -4
- package/lib-es/network/serialization.d.ts.map +1 -1
- package/lib-es/network/serialization.js +97 -95
- package/lib-es/network/serialization.js.map +1 -1
- package/lib-es/polyfill.d.ts.map +1 -1
- package/lib-es/polyfill.js +7 -5
- package/lib-es/polyfill.js.map +1 -1
- package/lib-es/supportedFeatures.d.ts +1 -1
- package/lib-es/supportedFeatures.d.ts.map +1 -1
- package/lib-es/supportedFeatures.js +3 -1
- package/lib-es/supportedFeatures.js.map +1 -1
- package/lib-es/types/errors.d.ts.map +1 -1
- package/lib-es/types/errors.js +15 -13
- package/lib-es/types/errors.js.map +1 -1
- package/lib-es/types/index.d.ts +2 -2
- package/lib-es/types/index.d.ts.map +1 -1
- package/lib-es/types/index.js +4 -2
- package/lib-es/types/index.js.map +1 -1
- package/lib-es/types/model.d.ts +6 -6
- package/lib-es/types/model.d.ts.map +1 -1
- package/lib-es/types/model.js +7 -5
- package/lib-es/types/model.js.map +1 -1
- package/package.json +13 -16
- package/src/api/index.integ.test.ts +168 -163
- package/src/api/index.test.ts +106 -103
- package/src/api/index.ts +49 -46
- package/src/api/index_error.test.ts +48 -25
- package/src/config.ts +15 -12
- package/src/constants.ts +4 -1
- package/src/globals.d.ts +7 -5
- package/src/logic/broadcast.integ.test.ts +22 -19
- package/src/logic/broadcast.ts +5 -2
- package/src/logic/combine.test.ts +18 -15
- package/src/logic/combine.ts +8 -5
- package/src/logic/common.ts +9 -6
- package/src/logic/craftTransaction.test.ts +76 -75
- package/src/logic/craftTransaction.ts +46 -43
- package/src/logic/estimateFees.ts +6 -3
- package/src/logic/getBalance.test.ts +31 -28
- package/src/logic/getBalance.ts +13 -10
- package/src/logic/getBlock.integ.test.ts +40 -39
- package/src/logic/getBlock.test.ts +462 -415
- package/src/logic/getBlock.ts +142 -127
- package/src/logic/getBlockInfo.test.ts +72 -69
- package/src/logic/getBlockInfo.ts +15 -12
- package/src/logic/index.ts +15 -12
- package/src/logic/lastBlock.ts +6 -3
- package/src/logic/listOperations.ts +14 -11
- package/src/logic/listOperations.unit.test.ts +212 -209
- package/src/logic/networkWrappers.test.ts +32 -29
- package/src/logic/sdkWrapper.ts +19 -16
- package/src/logic/utils.ts +13 -10
- package/src/logic/validateAddress.test.ts +21 -18
- package/src/logic/validateAddress.ts +7 -4
- package/src/logic/validateIntent.test.ts +62 -59
- package/src/logic/validateIntent.ts +87 -84
- package/src/logic/validateMemo.test.ts +50 -47
- package/src/logic/validateMemo.ts +16 -13
- package/src/network/broadcastTransaction.test.ts +83 -63
- package/src/network/fetchLedgerOperations.unit.test.ts +155 -0
- package/src/network/horizon.integ.test.ts +45 -42
- package/src/network/horizon.test.ts +17 -14
- package/src/network/horizon.ts +244 -314
- package/src/network/horizon.unit.test.ts +238 -237
- package/src/network/horizonErrorBody.test.ts +133 -0
- package/src/network/horizonErrorBody.ts +178 -0
- package/src/network/horizonLedgerErrors.test.ts +150 -78
- package/src/network/horizonLedgerErrors.ts +47 -20
- package/src/network/index.ts +4 -1
- package/src/network/registerHorizonInterceptors.test.ts +163 -160
- package/src/network/serialization.ts +232 -227
- package/src/network/serialization.unit.test.ts +801 -798
- package/src/polyfill.ts +18 -15
- package/src/supportedFeatures.ts +6 -3
- package/src/types/errors.ts +33 -32
- package/src/types/index.ts +5 -2
- package/src/types/model.ts +55 -52
- package/tsconfig.json +1 -1
- package/.unimportedrc.json +0 -17
- package/LICENSE.txt +0 -21
- package/src/network/fetchLedgerOperations.nock.unit.test.ts +0 -156
package/src/logic/getBlock.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { RawOperation, StellarMemo } from '../types'
|
|
1
5
|
import type {
|
|
2
6
|
AssetInfo,
|
|
3
7
|
Block,
|
|
4
8
|
BlockInfo,
|
|
5
9
|
BlockOperation,
|
|
6
10
|
BlockTransaction,
|
|
7
|
-
} from
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
11
|
+
} from '@ledgerhq/coin-module-framework/api/index'
|
|
12
|
+
import BigNumber from 'bignumber.js'
|
|
13
|
+
import { fetchAllLedgerOperations, fetchLedgerRecord } from '../network'
|
|
14
|
+
import { decodeMemo, parseSequence } from '../network/serialization'
|
|
15
|
+
import { parseAPIValue } from './common'
|
|
16
|
+
import { assertUnreachable } from './utils'
|
|
13
17
|
|
|
14
|
-
const NATIVE_ASSET: AssetInfo = { type:
|
|
18
|
+
const NATIVE_ASSET: AssetInfo = { type: 'native', name: 'XLM' }
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Horizon `type` strings that `getBlock` maps into `BlockOperation` entries.
|
|
@@ -20,148 +24,148 @@ const NATIVE_ASSET: AssetInfo = { type: "native", name: "XLM" };
|
|
|
20
24
|
* other operation kinds are ignored until a later iteration expands coverage.
|
|
21
25
|
*/
|
|
22
26
|
const SUPPORTED_GETBLOCK_OP_TYPES = new Set<string>([
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
])
|
|
27
|
+
'create_account',
|
|
28
|
+
'payment',
|
|
29
|
+
'path_payment_strict_send',
|
|
30
|
+
'path_payment_strict_receive',
|
|
31
|
+
'change_trust',
|
|
32
|
+
])
|
|
29
33
|
|
|
30
|
-
type RawPathStrictSendOp = Extract<RawOperation, { type:
|
|
31
|
-
type RawPathStrictReceiveOp = Extract<RawOperation, { type:
|
|
32
|
-
send_max?: string
|
|
33
|
-
}
|
|
34
|
+
type RawPathStrictSendOp = Extract<RawOperation, { type: 'path_payment_strict_send' }>
|
|
35
|
+
type RawPathStrictReceiveOp = Extract<RawOperation, { type: 'path_payment_strict_receive' }> & {
|
|
36
|
+
send_max?: string
|
|
37
|
+
}
|
|
34
38
|
|
|
35
39
|
/** Horizon ops mapped by `getBlock`; kept in sync with `SUPPORTED_GETBLOCK_OP_TYPES`. */
|
|
36
40
|
type SupportedGetBlockOperation = Extract<
|
|
37
41
|
RawOperation,
|
|
38
|
-
| { type:
|
|
39
|
-
| { type:
|
|
40
|
-
| { type:
|
|
41
|
-
| { type:
|
|
42
|
-
| { type:
|
|
43
|
-
|
|
42
|
+
| { type: 'payment' }
|
|
43
|
+
| { type: 'create_account' }
|
|
44
|
+
| { type: 'change_trust' }
|
|
45
|
+
| { type: 'path_payment_strict_send' }
|
|
46
|
+
| { type: 'path_payment_strict_receive' }
|
|
47
|
+
>
|
|
44
48
|
|
|
45
49
|
function pathStrictDualLegTransfers(params: {
|
|
46
|
-
fromAddr: string | undefined
|
|
47
|
-
toAddr: string | undefined
|
|
48
|
-
sourceAsset: AssetInfo
|
|
49
|
-
destAsset: AssetInfo
|
|
50
|
-
sourceStroops: bigint
|
|
51
|
-
destStroops: bigint
|
|
50
|
+
fromAddr: string | undefined
|
|
51
|
+
toAddr: string | undefined
|
|
52
|
+
sourceAsset: AssetInfo
|
|
53
|
+
destAsset: AssetInfo
|
|
54
|
+
sourceStroops: bigint
|
|
55
|
+
destStroops: bigint
|
|
52
56
|
}): BlockOperation[] {
|
|
53
|
-
const { fromAddr, toAddr, sourceAsset, destAsset, sourceStroops, destStroops } = params
|
|
54
|
-
const ops: BlockOperation[] = []
|
|
57
|
+
const { fromAddr, toAddr, sourceAsset, destAsset, sourceStroops, destStroops } = params
|
|
58
|
+
const ops: BlockOperation[] = []
|
|
55
59
|
if (fromAddr && sourceStroops !== 0n) {
|
|
56
60
|
ops.push({
|
|
57
|
-
type:
|
|
61
|
+
type: 'transfer',
|
|
58
62
|
address: fromAddr,
|
|
59
63
|
...(toAddr && { peer: toAddr }),
|
|
60
64
|
asset: sourceAsset,
|
|
61
65
|
amount: -sourceStroops,
|
|
62
|
-
})
|
|
66
|
+
})
|
|
63
67
|
}
|
|
64
68
|
if (toAddr && destStroops !== 0n) {
|
|
65
69
|
ops.push({
|
|
66
|
-
type:
|
|
70
|
+
type: 'transfer',
|
|
67
71
|
address: toAddr,
|
|
68
72
|
...(fromAddr && { peer: fromAddr }),
|
|
69
73
|
asset: destAsset,
|
|
70
74
|
amount: destStroops,
|
|
71
|
-
})
|
|
75
|
+
})
|
|
72
76
|
}
|
|
73
|
-
return ops
|
|
77
|
+
return ops
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
function toStroops(amountStr: string | undefined): bigint {
|
|
77
81
|
if (!amountStr) {
|
|
78
|
-
return 0n
|
|
82
|
+
return 0n
|
|
79
83
|
}
|
|
80
|
-
return BigInt(parseAPIValue(amountStr).integerValue(BigNumber.ROUND_FLOOR).toFixed(0))
|
|
84
|
+
return BigInt(parseAPIValue(amountStr).integerValue(BigNumber.ROUND_FLOOR).toFixed(0))
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
function assetFromHorizonFields(
|
|
84
88
|
assetType: string | undefined,
|
|
85
89
|
assetCode: string | undefined,
|
|
86
|
-
assetIssuer: string | undefined
|
|
90
|
+
assetIssuer: string | undefined
|
|
87
91
|
): AssetInfo {
|
|
88
|
-
if (!assetType || assetType ===
|
|
89
|
-
return NATIVE_ASSET
|
|
92
|
+
if (!assetType || assetType === 'native') {
|
|
93
|
+
return NATIVE_ASSET
|
|
90
94
|
}
|
|
91
95
|
return {
|
|
92
|
-
type:
|
|
93
|
-
assetReference: assetCode ??
|
|
94
|
-
assetOwner: assetIssuer ??
|
|
95
|
-
}
|
|
96
|
+
type: 'token',
|
|
97
|
+
assetReference: assetCode ?? '',
|
|
98
|
+
assetOwner: assetIssuer ?? '',
|
|
99
|
+
}
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
function isSupportedGetBlockOperation(op: RawOperation): op is SupportedGetBlockOperation {
|
|
99
|
-
return SUPPORTED_GETBLOCK_OP_TYPES.has(op.type)
|
|
103
|
+
return SUPPORTED_GETBLOCK_OP_TYPES.has(op.type)
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
function orderTransactionHashes(rawOps: RawOperation[]): string[] {
|
|
103
|
-
const order: string[] = []
|
|
104
|
-
const seen = new Set<string>()
|
|
107
|
+
const order: string[] = []
|
|
108
|
+
const seen = new Set<string>()
|
|
105
109
|
for (const op of rawOps) {
|
|
106
|
-
const h = op.transaction_hash
|
|
110
|
+
const h = op.transaction_hash
|
|
107
111
|
if (!seen.has(h)) {
|
|
108
|
-
seen.add(h)
|
|
109
|
-
order.push(h)
|
|
112
|
+
seen.add(h)
|
|
113
|
+
order.push(h)
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
|
-
return order
|
|
116
|
+
return order
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
function groupOpsByHash(rawOps: RawOperation[]): Map<string, RawOperation[]> {
|
|
116
|
-
const map = new Map<string, RawOperation[]>()
|
|
120
|
+
const map = new Map<string, RawOperation[]>()
|
|
117
121
|
for (const op of rawOps) {
|
|
118
|
-
const h = op.transaction_hash
|
|
119
|
-
const list = map.get(h)
|
|
120
|
-
if (list) list.push(op)
|
|
121
|
-
else map.set(h, [op])
|
|
122
|
+
const h = op.transaction_hash
|
|
123
|
+
const list = map.get(h)
|
|
124
|
+
if (list) list.push(op)
|
|
125
|
+
else map.set(h, [op])
|
|
122
126
|
}
|
|
123
|
-
return map
|
|
127
|
+
return map
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
function mapPaymentLikeTransfer(
|
|
127
131
|
fromAddr: string | undefined,
|
|
128
132
|
toAddr: string | undefined,
|
|
129
133
|
amountStr: string | undefined,
|
|
130
|
-
asset: AssetInfo
|
|
134
|
+
asset: AssetInfo
|
|
131
135
|
): BlockOperation[] {
|
|
132
|
-
const amount = toStroops(amountStr)
|
|
136
|
+
const amount = toStroops(amountStr)
|
|
133
137
|
if (amount === 0n) {
|
|
134
|
-
return []
|
|
138
|
+
return []
|
|
135
139
|
}
|
|
136
|
-
const ops: BlockOperation[] = []
|
|
140
|
+
const ops: BlockOperation[] = []
|
|
137
141
|
if (fromAddr) {
|
|
138
142
|
ops.push({
|
|
139
|
-
type:
|
|
143
|
+
type: 'transfer',
|
|
140
144
|
address: fromAddr,
|
|
141
145
|
...(toAddr && { peer: toAddr }),
|
|
142
146
|
asset,
|
|
143
147
|
amount: -amount,
|
|
144
|
-
})
|
|
148
|
+
})
|
|
145
149
|
}
|
|
146
150
|
if (toAddr) {
|
|
147
151
|
ops.push({
|
|
148
|
-
type:
|
|
152
|
+
type: 'transfer',
|
|
149
153
|
address: toAddr,
|
|
150
154
|
...(fromAddr && { peer: fromAddr }),
|
|
151
155
|
asset,
|
|
152
156
|
amount,
|
|
153
|
-
})
|
|
157
|
+
})
|
|
154
158
|
}
|
|
155
|
-
return ops
|
|
159
|
+
return ops
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
function blockOperationsPathStrictSend(op: RawPathStrictSendOp): BlockOperation[] {
|
|
159
|
-
const destAsset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer)
|
|
163
|
+
const destAsset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer)
|
|
160
164
|
const sourceAsset = assetFromHorizonFields(
|
|
161
165
|
op.source_asset_type,
|
|
162
166
|
op.source_asset_code,
|
|
163
|
-
op.source_asset_issuer
|
|
164
|
-
)
|
|
167
|
+
op.source_asset_issuer
|
|
168
|
+
)
|
|
165
169
|
return pathStrictDualLegTransfers({
|
|
166
170
|
fromAddr: op.from,
|
|
167
171
|
toAddr: op.to,
|
|
@@ -169,18 +173,18 @@ function blockOperationsPathStrictSend(op: RawPathStrictSendOp): BlockOperation[
|
|
|
169
173
|
destAsset,
|
|
170
174
|
sourceStroops: toStroops(op.source_amount),
|
|
171
175
|
destStroops: toStroops(op.amount),
|
|
172
|
-
})
|
|
176
|
+
})
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
function blockOperationsPathStrictReceive(op: RawPathStrictReceiveOp): BlockOperation[] {
|
|
176
|
-
const destAsset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer)
|
|
180
|
+
const destAsset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer)
|
|
177
181
|
const sourceAsset = assetFromHorizonFields(
|
|
178
182
|
op.source_asset_type,
|
|
179
183
|
op.source_asset_code,
|
|
180
|
-
op.source_asset_issuer
|
|
181
|
-
)
|
|
184
|
+
op.source_asset_issuer
|
|
185
|
+
)
|
|
182
186
|
// Horizon reports actual source debit in `source_amount`; `send_max` is only the envelope cap.
|
|
183
|
-
const sourceStroops = toStroops(op.source_amount ?? op.send_max)
|
|
187
|
+
const sourceStroops = toStroops(op.source_amount ?? op.send_max)
|
|
184
188
|
return pathStrictDualLegTransfers({
|
|
185
189
|
fromAddr: op.from,
|
|
186
190
|
toAddr: op.to,
|
|
@@ -188,63 +192,74 @@ function blockOperationsPathStrictReceive(op: RawPathStrictReceiveOp): BlockOper
|
|
|
188
192
|
destAsset,
|
|
189
193
|
sourceStroops,
|
|
190
194
|
destStroops: toStroops(op.amount),
|
|
191
|
-
})
|
|
195
|
+
})
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
type TxContext = {
|
|
199
|
+
memo: StellarMemo | undefined
|
|
200
|
+
sequence: string | undefined
|
|
201
|
+
feeCharged: string
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function mapSupportedOperationToBlockOperations(
|
|
205
|
+
op: SupportedGetBlockOperation,
|
|
206
|
+
txCtx: TxContext
|
|
207
|
+
): BlockOperation[] {
|
|
208
|
+
if (op.type === 'payment') {
|
|
209
|
+
const asset = assetFromHorizonFields(op.asset_type, op.asset_code, op.asset_issuer)
|
|
210
|
+
const toAddr = op.to_muxed || op.to
|
|
211
|
+
return mapPaymentLikeTransfer(op.from, toAddr, op.amount, asset)
|
|
199
212
|
}
|
|
200
|
-
if (op.type ===
|
|
201
|
-
return mapPaymentLikeTransfer(op.funder, op.account, op.starting_balance, NATIVE_ASSET)
|
|
213
|
+
if (op.type === 'create_account') {
|
|
214
|
+
return mapPaymentLikeTransfer(op.funder, op.account, op.starting_balance, NATIVE_ASSET)
|
|
202
215
|
}
|
|
203
|
-
if (op.type ===
|
|
204
|
-
const isOptOut = new BigNumber(op.limit ||
|
|
216
|
+
if (op.type === 'change_trust') {
|
|
217
|
+
const isOptOut = new BigNumber(op.limit || '0').eq(0)
|
|
218
|
+
// Align with listOperations: assetAmount = fee_charged (not the trustline limit)
|
|
205
219
|
return [
|
|
206
220
|
{
|
|
207
|
-
type:
|
|
208
|
-
ledgerOpType: isOptOut ?
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
limit: op.limit,
|
|
221
|
+
type: 'other',
|
|
222
|
+
ledgerOpType: isOptOut ? 'OPT_OUT' : 'OPT_IN',
|
|
223
|
+
assetAmount: op.asset_code ? txCtx.feeCharged : undefined,
|
|
224
|
+
...(txCtx.memo && { memo: txCtx.memo }),
|
|
225
|
+
...(txCtx.sequence && { sequence: txCtx.sequence }),
|
|
213
226
|
},
|
|
214
|
-
]
|
|
227
|
+
]
|
|
215
228
|
}
|
|
216
|
-
if (op.type ===
|
|
217
|
-
return blockOperationsPathStrictSend(op)
|
|
229
|
+
if (op.type === 'path_payment_strict_send') {
|
|
230
|
+
return blockOperationsPathStrictSend(op)
|
|
218
231
|
}
|
|
219
|
-
if (op.type ===
|
|
220
|
-
return blockOperationsPathStrictReceive(op)
|
|
232
|
+
if (op.type === 'path_payment_strict_receive') {
|
|
233
|
+
return blockOperationsPathStrictReceive(op)
|
|
221
234
|
}
|
|
222
|
-
return assertUnreachable(op)
|
|
235
|
+
return assertUnreachable(op)
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
async function blockTransactionForHash(
|
|
226
239
|
hash: string,
|
|
227
|
-
ops: RawOperation[]
|
|
240
|
+
ops: RawOperation[]
|
|
228
241
|
): Promise<BlockTransaction | null> {
|
|
229
|
-
if (ops.length === 0) return null
|
|
230
|
-
const failed = !ops[0].transaction_successful
|
|
242
|
+
if (ops.length === 0) return null
|
|
243
|
+
const failed = !ops[0].transaction_successful
|
|
231
244
|
|
|
232
|
-
|
|
245
|
+
const tx = await ops[0].transaction()
|
|
246
|
+
const txCtx: TxContext = {
|
|
247
|
+
memo: decodeMemo(tx),
|
|
248
|
+
sequence: parseSequence(tx.source_account_sequence),
|
|
249
|
+
feeCharged: new BigNumber(tx.fee_charged || '0').toString(),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let blockOperations: BlockOperation[] = []
|
|
233
253
|
if (!failed) {
|
|
234
254
|
for (const op of ops) {
|
|
235
255
|
if (isSupportedGetBlockOperation(op)) {
|
|
236
|
-
blockOperations = blockOperations.concat(mapSupportedOperationToBlockOperations(op))
|
|
256
|
+
blockOperations = blockOperations.concat(mapSupportedOperationToBlockOperations(op, txCtx))
|
|
237
257
|
}
|
|
238
258
|
}
|
|
239
259
|
}
|
|
240
260
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const tx = await ops[0].transaction();
|
|
246
|
-
const fees = BigInt(tx.fee_charged || "0");
|
|
247
|
-
const feesPayer = tx.fee_account || tx.source_account;
|
|
261
|
+
const fees = BigInt(tx.fee_charged || '0')
|
|
262
|
+
const feesPayer = tx.fee_account || tx.source_account
|
|
248
263
|
|
|
249
264
|
return {
|
|
250
265
|
hash,
|
|
@@ -252,24 +267,24 @@ async function blockTransactionForHash(
|
|
|
252
267
|
fees,
|
|
253
268
|
...(feesPayer ? { feesPayer } : {}),
|
|
254
269
|
operations: blockOperations,
|
|
255
|
-
}
|
|
270
|
+
}
|
|
256
271
|
}
|
|
257
272
|
|
|
258
273
|
async function buildBlockTransactions(rawOps: RawOperation[]): Promise<BlockTransaction[]> {
|
|
259
|
-
const order = orderTransactionHashes(rawOps)
|
|
260
|
-
const byHash = groupOpsByHash(rawOps)
|
|
261
|
-
const out: BlockTransaction[] = []
|
|
274
|
+
const order = orderTransactionHashes(rawOps)
|
|
275
|
+
const byHash = groupOpsByHash(rawOps)
|
|
276
|
+
const out: BlockTransaction[] = []
|
|
262
277
|
|
|
263
278
|
for (const hash of order) {
|
|
264
279
|
// `order` and `byHash` are built from the same `rawOps`; every ordered hash has a non-empty op list.
|
|
265
|
-
const ops = byHash.get(hash)
|
|
266
|
-
const row = await blockTransactionForHash(hash, ops)
|
|
280
|
+
const ops = byHash.get(hash)!
|
|
281
|
+
const row = await blockTransactionForHash(hash, ops)
|
|
267
282
|
if (row) {
|
|
268
|
-
out.push(row)
|
|
283
|
+
out.push(row)
|
|
269
284
|
}
|
|
270
285
|
}
|
|
271
286
|
|
|
272
|
-
return out
|
|
287
|
+
return out
|
|
273
288
|
}
|
|
274
289
|
|
|
275
290
|
/**
|
|
@@ -284,22 +299,22 @@ async function buildBlockTransactions(rawOps: RawOperation[]): Promise<BlockTran
|
|
|
284
299
|
*/
|
|
285
300
|
export async function getBlock(height: number): Promise<Block> {
|
|
286
301
|
if (!Number.isSafeInteger(height) || height <= 0) {
|
|
287
|
-
throw new Error(`getBlock: height must be a positive integer, got ${height}`)
|
|
302
|
+
throw new Error(`getBlock: height must be a positive integer, got ${height}`)
|
|
288
303
|
}
|
|
289
304
|
|
|
290
305
|
const [ledger, rawOps] = await Promise.all([
|
|
291
306
|
fetchLedgerRecord(height),
|
|
292
307
|
fetchAllLedgerOperations(height),
|
|
293
|
-
])
|
|
308
|
+
])
|
|
294
309
|
|
|
295
|
-
let parent: BlockInfo[
|
|
310
|
+
let parent: BlockInfo['parent']
|
|
296
311
|
if (height > 1) {
|
|
297
|
-
const prevHash = (ledger as { prev_hash?: string }).prev_hash
|
|
312
|
+
const prevHash = (ledger as { prev_hash?: string }).prev_hash
|
|
298
313
|
if (prevHash) {
|
|
299
|
-
parent = { height: height - 1, hash: prevHash }
|
|
314
|
+
parent = { height: height - 1, hash: prevHash }
|
|
300
315
|
} else {
|
|
301
|
-
const parentLedger = await fetchLedgerRecord(height - 1)
|
|
302
|
-
parent = { height: parentLedger.sequence, hash: parentLedger.hash }
|
|
316
|
+
const parentLedger = await fetchLedgerRecord(height - 1)
|
|
317
|
+
parent = { height: parentLedger.sequence, hash: parentLedger.hash }
|
|
303
318
|
}
|
|
304
319
|
}
|
|
305
320
|
|
|
@@ -308,8 +323,8 @@ export async function getBlock(height: number): Promise<Block> {
|
|
|
308
323
|
hash: ledger.hash,
|
|
309
324
|
time: new Date(ledger.closed_at),
|
|
310
325
|
...(parent && { parent }),
|
|
311
|
-
}
|
|
326
|
+
}
|
|
312
327
|
|
|
313
|
-
const transactions = await buildBlockTransactions(rawOps)
|
|
314
|
-
return { info, transactions }
|
|
328
|
+
const transactions = await buildBlockTransactions(rawOps)
|
|
329
|
+
return { info, transactions }
|
|
315
330
|
}
|
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import * as network from '../network'
|
|
5
|
+
import { getBlockInfo } from './getBlockInfo'
|
|
6
|
+
|
|
7
|
+
jest.mock('../network', () => ({
|
|
8
|
+
...jest.requireActual<typeof import('../network')>('../network'),
|
|
6
9
|
fetchLedgerRecord: jest.fn(),
|
|
7
|
-
}))
|
|
10
|
+
}))
|
|
8
11
|
|
|
9
12
|
const fetchLedgerRecordMock = network.fetchLedgerRecord as jest.MockedFunction<
|
|
10
13
|
typeof network.fetchLedgerRecord
|
|
11
|
-
|
|
14
|
+
>
|
|
12
15
|
|
|
13
|
-
describe(
|
|
16
|
+
describe('getBlockInfo', () => {
|
|
14
17
|
beforeEach(() => {
|
|
15
|
-
jest.clearAllMocks()
|
|
16
|
-
})
|
|
18
|
+
jest.clearAllMocks()
|
|
19
|
+
})
|
|
17
20
|
|
|
18
|
-
it(
|
|
21
|
+
it('returns block info with parent when height > 1 (uses ledger prev_hash)', async () => {
|
|
19
22
|
fetchLedgerRecordMock.mockImplementation(
|
|
20
23
|
async (seq: number) =>
|
|
21
24
|
({
|
|
@@ -23,94 +26,94 @@ describe("getBlockInfo", () => {
|
|
|
23
26
|
hash: `hash-${seq}`,
|
|
24
27
|
closed_at: `2020-01-0${seq}T00:00:00Z`,
|
|
25
28
|
...(seq > 1 && { prev_hash: `hash-${seq - 1}` }),
|
|
26
|
-
}) as unknown as Awaited<ReturnType<typeof network.fetchLedgerRecord
|
|
27
|
-
)
|
|
29
|
+
}) as unknown as Awaited<ReturnType<typeof network.fetchLedgerRecord>>
|
|
30
|
+
)
|
|
28
31
|
|
|
29
|
-
const result = await getBlockInfo(5)
|
|
32
|
+
const result = await getBlockInfo(5)
|
|
30
33
|
|
|
31
|
-
expect(fetchLedgerRecordMock).toHaveBeenCalledTimes(1)
|
|
32
|
-
expect(fetchLedgerRecordMock).toHaveBeenCalledWith(5)
|
|
34
|
+
expect(fetchLedgerRecordMock).toHaveBeenCalledTimes(1)
|
|
35
|
+
expect(fetchLedgerRecordMock).toHaveBeenCalledWith(5)
|
|
33
36
|
expect(result).toEqual({
|
|
34
37
|
height: 5,
|
|
35
|
-
hash:
|
|
36
|
-
time: new Date(
|
|
37
|
-
parent: { height: 4, hash:
|
|
38
|
-
})
|
|
39
|
-
})
|
|
38
|
+
hash: 'hash-5',
|
|
39
|
+
time: new Date('2020-01-05T00:00:00Z'),
|
|
40
|
+
parent: { height: 4, hash: 'hash-4' },
|
|
41
|
+
})
|
|
42
|
+
})
|
|
40
43
|
|
|
41
|
-
it(
|
|
44
|
+
it('falls back to fetching parent ledger when prev_hash is missing', async () => {
|
|
42
45
|
fetchLedgerRecordMock.mockImplementation(
|
|
43
46
|
async (seq: number) =>
|
|
44
47
|
({
|
|
45
48
|
sequence: seq,
|
|
46
49
|
hash: `hash-${seq}`,
|
|
47
50
|
closed_at: `2020-01-0${seq}T00:00:00Z`,
|
|
48
|
-
}) as unknown as Awaited<ReturnType<typeof network.fetchLedgerRecord
|
|
49
|
-
)
|
|
51
|
+
}) as unknown as Awaited<ReturnType<typeof network.fetchLedgerRecord>>
|
|
52
|
+
)
|
|
50
53
|
|
|
51
|
-
const result = await getBlockInfo(5)
|
|
54
|
+
const result = await getBlockInfo(5)
|
|
52
55
|
|
|
53
|
-
expect(fetchLedgerRecordMock).toHaveBeenCalledWith(5)
|
|
54
|
-
expect(fetchLedgerRecordMock).toHaveBeenCalledWith(4)
|
|
55
|
-
expect(fetchLedgerRecordMock).toHaveBeenCalledTimes(2)
|
|
56
|
+
expect(fetchLedgerRecordMock).toHaveBeenCalledWith(5)
|
|
57
|
+
expect(fetchLedgerRecordMock).toHaveBeenCalledWith(4)
|
|
58
|
+
expect(fetchLedgerRecordMock).toHaveBeenCalledTimes(2)
|
|
56
59
|
expect(result).toEqual({
|
|
57
60
|
height: 5,
|
|
58
|
-
hash:
|
|
59
|
-
time: new Date(
|
|
60
|
-
parent: { height: 4, hash:
|
|
61
|
-
})
|
|
62
|
-
})
|
|
61
|
+
hash: 'hash-5',
|
|
62
|
+
time: new Date('2020-01-05T00:00:00Z'),
|
|
63
|
+
parent: { height: 4, hash: 'hash-4' },
|
|
64
|
+
})
|
|
65
|
+
})
|
|
63
66
|
|
|
64
|
-
it(
|
|
67
|
+
it('omits parent for genesis ledger sequence 1', async () => {
|
|
65
68
|
fetchLedgerRecordMock.mockResolvedValue({
|
|
66
69
|
sequence: 1,
|
|
67
|
-
hash:
|
|
68
|
-
closed_at:
|
|
69
|
-
} as unknown as Awaited<ReturnType<typeof network.fetchLedgerRecord>>)
|
|
70
|
+
hash: 'genesis-hash',
|
|
71
|
+
closed_at: '2015-09-30T00:00:00Z',
|
|
72
|
+
} as unknown as Awaited<ReturnType<typeof network.fetchLedgerRecord>>)
|
|
70
73
|
|
|
71
|
-
const result = await getBlockInfo(1)
|
|
74
|
+
const result = await getBlockInfo(1)
|
|
72
75
|
|
|
73
|
-
expect(fetchLedgerRecordMock).toHaveBeenCalledTimes(1)
|
|
74
|
-
expect(fetchLedgerRecordMock).toHaveBeenCalledWith(1)
|
|
75
|
-
expect(result.parent).toBeUndefined()
|
|
76
|
-
expect(result.height).toBe(1)
|
|
77
|
-
})
|
|
76
|
+
expect(fetchLedgerRecordMock).toHaveBeenCalledTimes(1)
|
|
77
|
+
expect(fetchLedgerRecordMock).toHaveBeenCalledWith(1)
|
|
78
|
+
expect(result.parent).toBeUndefined()
|
|
79
|
+
expect(result.height).toBe(1)
|
|
80
|
+
})
|
|
78
81
|
|
|
79
|
-
describe(
|
|
80
|
-
it(
|
|
82
|
+
describe('height validation', () => {
|
|
83
|
+
it('rejects height 0', async () => {
|
|
81
84
|
await expect(getBlockInfo(0)).rejects.toThrow(
|
|
82
|
-
|
|
83
|
-
)
|
|
84
|
-
})
|
|
85
|
+
'getBlockInfo: height must be a positive integer, got 0'
|
|
86
|
+
)
|
|
87
|
+
})
|
|
85
88
|
|
|
86
|
-
it(
|
|
89
|
+
it('rejects negative height', async () => {
|
|
87
90
|
await expect(getBlockInfo(-3)).rejects.toThrow(
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
})
|
|
91
|
+
'getBlockInfo: height must be a positive integer, got -3'
|
|
92
|
+
)
|
|
93
|
+
})
|
|
91
94
|
|
|
92
|
-
it(
|
|
95
|
+
it('rejects non-integer height', async () => {
|
|
93
96
|
await expect(getBlockInfo(3.5)).rejects.toThrow(
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
})
|
|
97
|
+
'getBlockInfo: height must be a positive integer, got 3.5'
|
|
98
|
+
)
|
|
99
|
+
})
|
|
97
100
|
|
|
98
|
-
it(
|
|
101
|
+
it('rejects NaN', async () => {
|
|
99
102
|
await expect(getBlockInfo(Number.NaN)).rejects.toThrow(
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
})
|
|
103
|
+
'getBlockInfo: height must be a positive integer, got NaN'
|
|
104
|
+
)
|
|
105
|
+
})
|
|
103
106
|
|
|
104
|
-
it(
|
|
107
|
+
it('rejects unsafe integer', async () => {
|
|
105
108
|
await expect(getBlockInfo(Number.MAX_SAFE_INTEGER + 1)).rejects.toThrow(
|
|
106
|
-
|
|
107
|
-
)
|
|
108
|
-
})
|
|
109
|
+
'getBlockInfo: height must be a positive integer'
|
|
110
|
+
)
|
|
111
|
+
})
|
|
109
112
|
|
|
110
|
-
it(
|
|
113
|
+
it('rejects Infinity', async () => {
|
|
111
114
|
await expect(getBlockInfo(Number.POSITIVE_INFINITY)).rejects.toThrow(
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
})
|
|
115
|
+
'getBlockInfo: height must be a positive integer, got Infinity'
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|