@shipload/sdk 1.0.0-next.62 → 1.0.0-next.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/lib/shipload.d.ts +1829 -501
  2. package/lib/shipload.js +4325 -1324
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +4286 -1389
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +391 -13
  7. package/lib/testing.js +1692 -107
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +1693 -108
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +2 -2
  12. package/src/contracts/fund.ts +201 -0
  13. package/src/contracts/platform.ts +73 -1
  14. package/src/contracts/server.ts +946 -26
  15. package/src/contracts/token.ts +170 -0
  16. package/src/contracts.ts +2 -0
  17. package/src/data/catalog.test.ts +19 -0
  18. package/src/data/catalog.ts +6 -0
  19. package/src/data/charters.json +198 -0
  20. package/src/data/citizenry-adjectives.json +42 -0
  21. package/src/data/citizenry-nouns.json +50 -0
  22. package/src/data/entities.json +36 -0
  23. package/src/data/item-ids.ts +2 -0
  24. package/src/data/items.json +12 -0
  25. package/src/data/kind-registry.json +20 -3
  26. package/src/data/kind-registry.ts +5 -0
  27. package/src/data/metadata.ts +14 -0
  28. package/src/data/recipes.json +0 -48
  29. package/src/derivation/capabilities.ts +14 -0
  30. package/src/derivation/recipe-usage.test.ts +0 -2
  31. package/src/format.ts +9 -0
  32. package/src/index-module.ts +32 -2
  33. package/src/influence/categories.ts +32 -0
  34. package/src/influence/charters.test.ts +126 -0
  35. package/src/influence/charters.ts +120 -0
  36. package/src/influence/citizenry.ts +38 -0
  37. package/src/influence/constants.ts +63 -0
  38. package/src/influence/decay.ts +20 -0
  39. package/src/influence/decomp.ts +95 -0
  40. package/src/influence/demand.ts +106 -0
  41. package/src/influence/duration.ts +36 -0
  42. package/src/influence/index.ts +63 -0
  43. package/src/influence/influence.test.ts +317 -0
  44. package/src/influence/quality.ts +25 -0
  45. package/src/influence/valuation.ts +154 -0
  46. package/src/managers/actions.ts +227 -5
  47. package/src/managers/balances.test.ts +161 -0
  48. package/src/managers/balances.ts +93 -0
  49. package/src/managers/construction.ts +11 -1
  50. package/src/managers/context.ts +18 -0
  51. package/src/managers/entities.ts +11 -0
  52. package/src/managers/index.ts +15 -1
  53. package/src/managers/influence.test.ts +58 -0
  54. package/src/managers/influence.ts +347 -0
  55. package/src/managers/nft.test.ts +8 -1
  56. package/src/managers/nft.ts +56 -16
  57. package/src/nft/description.ts +7 -0
  58. package/src/scheduling/task-cargo.ts +1 -0
  59. package/src/shipload.ts +10 -0
  60. package/src/types.ts +23 -4
  61. package/src/utils/hash.ts +4 -0
  62. package/src/utils/system.ts +1 -5
@@ -0,0 +1,154 @@
1
+ import {getItem} from '../data/catalog'
2
+ import {categoryIndex} from './categories'
3
+ import {
4
+ INFLUENCE_ATOMIC_PER_POINT,
5
+ INFLUENCE_MASS_KG_MAX,
6
+ INFLUENCE_NEED_FP_MAX,
7
+ INFLUENCE_WEIGHT_FP_MAX,
8
+ MASS_KG_PER_TONNE,
9
+ NEED_FP_SCALE,
10
+ WEIGHT_FP_SCALE,
11
+ W_EFF_ATOMIC_PER_KG,
12
+ } from './constants'
13
+ import {findDecomp} from './decomp'
14
+ import type {DemandView} from './demand'
15
+ import {needForCategory} from './demand'
16
+ import {getStatCount, statsSumSq} from './quality'
17
+
18
+ const UINT64_MAX = (1n << 64n) - 1n
19
+
20
+ export interface InfluencePricing {
21
+ d1: number
22
+ weight(category: number, tier: number): bigint
23
+ }
24
+
25
+ export interface ValuedItem {
26
+ itemId: number
27
+ quantity: number
28
+ stats: bigint
29
+ }
30
+
31
+ export function weightedQualityAtomic(
32
+ weightedKgFp: bigint,
33
+ sumSq: number,
34
+ nStats: number,
35
+ d1: number,
36
+ needFp: bigint
37
+ ): bigint {
38
+ if (nStats <= 0 || d1 <= 0) throw new Error('influence: invalid quality divisor')
39
+ const num = weightedKgFp * BigInt(sumSq) * needFp
40
+ const den =
41
+ (BigInt(MASS_KG_PER_TONNE) *
42
+ BigInt(WEIGHT_FP_SCALE) *
43
+ BigInt(nStats) *
44
+ BigInt(d1) *
45
+ BigInt(NEED_FP_SCALE)) /
46
+ BigInt(INFLUENCE_ATOMIC_PER_POINT)
47
+ return num / den
48
+ }
49
+
50
+ export function resourceValueAtomic(
51
+ massKg: bigint,
52
+ weightFp: bigint,
53
+ sumSq: number,
54
+ nStats: number,
55
+ d1: number,
56
+ needFp: bigint
57
+ ): bigint {
58
+ if (massKg > INFLUENCE_MASS_KG_MAX) throw new Error('influence: mass out of range')
59
+ if (weightFp > INFLUENCE_WEIGHT_FP_MAX) throw new Error('influence: weight out of range')
60
+ if (needFp > INFLUENCE_NEED_FP_MAX) throw new Error('influence: need out of range')
61
+ return weightedQualityAtomic(massKg * weightFp, sumSq, nStats, d1, needFp)
62
+ }
63
+
64
+ export function componentBaseAtomic(
65
+ weightedRawKgFp: bigint,
66
+ sumSq: number,
67
+ nStats: number,
68
+ d1: number
69
+ ): bigint {
70
+ return weightedQualityAtomic(weightedRawKgFp, sumSq, nStats, d1, BigInt(NEED_FP_SCALE))
71
+ }
72
+
73
+ export function componentEffortAtomic(processedKgPerUnit: number, quantity: number): bigint {
74
+ const processed = BigInt(processedKgPerUnit) * BigInt(quantity)
75
+ if (processed > INFLUENCE_MASS_KG_MAX) {
76
+ throw new Error('influence: processed mass out of range')
77
+ }
78
+ return processed * W_EFF_ATOMIC_PER_KG
79
+ }
80
+
81
+ export function valueCargoItem(
82
+ item: ValuedItem,
83
+ demand: DemandView,
84
+ pricing: InfluencePricing
85
+ ): bigint {
86
+ const def = getItem(item.itemId)
87
+ if (def.type !== 'resource' && def.type !== 'component') {
88
+ throw new Error('item class cannot be valued')
89
+ }
90
+
91
+ const nStats = getStatCount(item.itemId)
92
+ const sumSq = statsSumSq(item.stats, nStats)
93
+
94
+ if (def.type === 'resource') {
95
+ if (def.category === undefined) throw new Error('resource has no category')
96
+ const category = categoryIndex(def.category)
97
+ const weightFp = pricing.weight(category, def.tier)
98
+ const needFp = needForCategory(demand, category)
99
+ const perUnit = resourceValueAtomic(
100
+ BigInt(def.mass),
101
+ weightFp,
102
+ sumSq,
103
+ nStats,
104
+ pricing.d1,
105
+ needFp
106
+ )
107
+ const total = perUnit * BigInt(item.quantity)
108
+ if (total > UINT64_MAX) throw new Error('influence: resource value overflow')
109
+ return total
110
+ }
111
+
112
+ const entry = findDecomp(item.itemId)
113
+ if (!entry) throw new Error('item has no decomposition')
114
+
115
+ let weighted = 0n
116
+ for (const bucket of entry.buckets) {
117
+ weighted += BigInt(bucket.rawKg) * pricing.weight(bucket.category, bucket.tier)
118
+ }
119
+
120
+ const perUnit = componentBaseAtomic(weighted, sumSq, nStats, pricing.d1)
121
+ const total = perUnit * BigInt(item.quantity)
122
+ if (total > UINT64_MAX) throw new Error('influence: component value overflow')
123
+
124
+ const withEffort = total + componentEffortAtomic(entry.processedKg, item.quantity)
125
+ if (withEffort > UINT64_MAX) throw new Error('influence: component value overflow')
126
+ return withEffort
127
+ }
128
+
129
+ export function valueContribution(
130
+ bundle: ValuedItem[],
131
+ demand: DemandView,
132
+ pricing: InfluencePricing
133
+ ): bigint {
134
+ let total = 0n
135
+ for (const item of bundle) total += valueCargoItem(item, demand, pricing)
136
+ if (total > UINT64_MAX) throw new Error('influence: contribution value overflow')
137
+ return total
138
+ }
139
+
140
+ export function pricingFromWeights(
141
+ d1: number,
142
+ weights: Iterable<{category: number; tier: number; weightFp: bigint | number | string}>
143
+ ): InfluencePricing {
144
+ const table = new Map<number, bigint>()
145
+ for (const row of weights) {
146
+ table.set(row.category * 16 + row.tier, BigInt(row.weightFp.toString()))
147
+ }
148
+ return {
149
+ d1,
150
+ weight(category: number, tier: number): bigint {
151
+ return table.get(category * 16 + tier) ?? 0n
152
+ },
153
+ }
154
+ }
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  Action,
3
+ Asset,
4
+ type AssetType,
3
5
  Bytes,
4
6
  Checksum256,
5
7
  type Checksum256Type,
8
+ ExtendedAsset,
6
9
  Int64,
7
10
  type Int64Type,
8
11
  Name,
@@ -18,7 +21,7 @@ import {
18
21
  } from '@wharfkit/antelope'
19
22
  import {BaseManager} from './base'
20
23
  import {Coordinates, PRECISION, type ClusterSlotType, type CoordinatesType} from '../types'
21
- import {ServerContract} from '../contracts'
24
+ import {ServerContract, TokenContract} from '../contracts'
22
25
  import {ATOMICASSETS_ABI, SHIPLOAD_COLLECTION} from '../nft/atomicassets'
23
26
  import {getItem} from '../data/catalog'
24
27
 
@@ -164,6 +167,8 @@ export type EntityRefInput = {
164
167
  export type WaypointInput = {x: Int64Type; y: Int64Type}
165
168
 
166
169
  export class ActionsManager extends BaseManager {
170
+ private tokenContracts = new Map<string, TokenContract.Contract>()
171
+
167
172
  travel(shipId: UInt64Type, destination: CoordinatesType, recharge = true): Action {
168
173
  const x = Int64.from(destination.x)
169
174
  const y = Int64.from(destination.y)
@@ -315,6 +320,30 @@ export class ActionsManager extends BaseManager {
315
320
  })
316
321
  }
317
322
 
323
+ depotstore(
324
+ shipId: UInt64Type,
325
+ depotId: UInt64Type,
326
+ items: ServerContract.ActionParams.Type.cargo_item[]
327
+ ): Action {
328
+ return this.server.action('depotstore', {
329
+ ship_id: UInt64.from(shipId),
330
+ depot_id: UInt64.from(depotId),
331
+ items,
332
+ })
333
+ }
334
+
335
+ depottake(
336
+ shipId: UInt64Type,
337
+ depotId: UInt64Type,
338
+ items: ServerContract.ActionParams.Type.cargo_item[]
339
+ ): Action {
340
+ return this.server.action('depottake', {
341
+ ship_id: UInt64.from(shipId),
342
+ depot_id: UInt64.from(depotId),
343
+ items,
344
+ })
345
+ }
346
+
318
347
  shuttle(
319
348
  carrierId: UInt64Type,
320
349
  fromId: UInt64Type,
@@ -511,6 +540,35 @@ export class ActionsManager extends BaseManager {
511
540
  return this.server.action('canceljob', params)
512
541
  }
513
542
 
543
+ upgradejob(
544
+ targetId: UInt64Type,
545
+ dockId: UInt64Type,
546
+ targetItemId: UInt16Type,
547
+ inputs: ServerContract.ActionParams.Type.cargo_item[]
548
+ ): Action {
549
+ const params: ServerContract.ActionParams.upgradejob = {
550
+ target_id: UInt64.from(targetId),
551
+ dock_id: UInt64.from(dockId),
552
+ target_item_id: UInt16.from(targetItemId),
553
+ inputs,
554
+ }
555
+ return this.server.action('upgradejob', params)
556
+ }
557
+
558
+ claimupgrade(jobId: UInt64Type): Action {
559
+ const params: ServerContract.ActionParams.claimupgrade = {
560
+ job_id: UInt64.from(jobId),
561
+ }
562
+ return this.server.action('claimupgrade', params)
563
+ }
564
+
565
+ cancelupgrade(jobId: UInt64Type): Action {
566
+ const params: ServerContract.ActionParams.cancelupgrade = {
567
+ job_id: UInt64.from(jobId),
568
+ }
569
+ return this.server.action('cancelupgrade', params)
570
+ }
571
+
514
572
  setsocket(workshopId: UInt64Type, slot: UInt8Type, open: boolean): Action {
515
573
  const params: ServerContract.ActionParams.setsocket = {
516
574
  workshop_id: UInt64.from(workshopId),
@@ -599,6 +657,12 @@ export class ActionsManager extends BaseManager {
599
657
  })
600
658
  }
601
659
 
660
+ abandonplot(plotId: UInt64Type): Action {
661
+ return this.server.action('abandonplot', {
662
+ plot_id: UInt64.from(plotId),
663
+ })
664
+ }
665
+
602
666
  addmodule(
603
667
  entityId: UInt64Type,
604
668
  moduleIndex: number,
@@ -637,6 +701,88 @@ export class ActionsManager extends BaseManager {
637
701
  })
638
702
  }
639
703
 
704
+ open(owner: NameType, tokenContract: NameType, tokenSymbol: Asset.SymbolType): Action {
705
+ return this.platform.action('open', {
706
+ owner: Name.from(owner),
707
+ token_contract: Name.from(tokenContract),
708
+ token_symbol: Asset.Symbol.from(tokenSymbol),
709
+ })
710
+ }
711
+
712
+ close(owner: NameType, tokenContract: NameType, tokenSymbol: Asset.SymbolType): Action {
713
+ return this.platform.action('close', {
714
+ owner: Name.from(owner),
715
+ token_contract: Name.from(tokenContract),
716
+ token_symbol: Asset.Symbol.from(tokenSymbol),
717
+ })
718
+ }
719
+
720
+ withdraw(
721
+ owner: NameType,
722
+ quantity: AssetType,
723
+ tokenContract: NameType,
724
+ memo = 'withdraw'
725
+ ): Action {
726
+ return this.platform.action('withdraw', {
727
+ owner: Name.from(owner),
728
+ quantity: ExtendedAsset.from({
729
+ quantity: Asset.from(quantity),
730
+ contract: Name.from(tokenContract),
731
+ }),
732
+ memo,
733
+ })
734
+ }
735
+
736
+ tokenContract(account: NameType): TokenContract.Contract {
737
+ const name = Name.from(account)
738
+ const key = String(name)
739
+ let contract = this.tokenContracts.get(key)
740
+ if (!contract) {
741
+ contract = new TokenContract.Contract({client: this.client, account: name})
742
+ this.tokenContracts.set(key, contract)
743
+ }
744
+ return contract
745
+ }
746
+
747
+ tokenTransfer(
748
+ from: NameType,
749
+ to: NameType,
750
+ quantity: AssetType,
751
+ tokenContract: NameType,
752
+ memo = ''
753
+ ): Action {
754
+ return this.tokenContract(tokenContract).action('transfer', {
755
+ from: Name.from(from),
756
+ to: Name.from(to),
757
+ quantity: Asset.from(quantity),
758
+ memo,
759
+ })
760
+ }
761
+
762
+ // `open` is an idempotent no-op; omitting it is what produces the "call open first" abort.
763
+ deposit(
764
+ owner: NameType,
765
+ quantity: AssetType,
766
+ tokenContract: NameType,
767
+ opts: {memo?: string; open?: boolean} = {}
768
+ ): Action[] {
769
+ const amount = Asset.from(quantity)
770
+ const actions: Action[] = []
771
+ if (opts.open !== false) {
772
+ actions.push(this.open(owner, tokenContract, amount.symbol))
773
+ }
774
+ actions.push(
775
+ this.tokenTransfer(
776
+ owner,
777
+ this.platform.account,
778
+ amount,
779
+ tokenContract,
780
+ opts.memo ?? 'deposit'
781
+ )
782
+ )
783
+ return actions
784
+ }
785
+
640
786
  async wrap(
641
787
  owner: NameType,
642
788
  entityId: UInt64Type,
@@ -655,8 +801,8 @@ export class ActionsManager extends BaseManager {
655
801
  quantity: UInt64.from(quantity),
656
802
  }),
657
803
  ]
658
- const claimRam =
659
- opts.claimRam ?? (this.atomicAssetsAccount ?? 'atomicassets') !== 'atomicassets'
804
+ // setlastpayer is what fires logrampayer and clears the wrap gate; skipping it strands it.
805
+ const claimRam = opts.claimRam ?? true
660
806
  if (claimRam) {
661
807
  actions.push(this.setLastPayer(owner, SHIPLOAD_COLLECTION))
662
808
  }
@@ -690,8 +836,8 @@ export class ActionsManager extends BaseManager {
690
836
  nexus_id: UInt64.from(nexusId),
691
837
  }),
692
838
  ]
693
- const claimRam =
694
- opts.claimRam ?? (this.atomicAssetsAccount ?? 'atomicassets') !== 'atomicassets'
839
+ // setlastpayer is what fires logrampayer and clears the wrap gate; skipping it strands it.
840
+ const claimRam = opts.claimRam ?? true
695
841
  if (claimRam) {
696
842
  actions.push(this.setLastPayer(owner, SHIPLOAD_COLLECTION))
697
843
  }
@@ -835,4 +981,80 @@ export class ActionsManager extends BaseManager {
835
981
  max_rows: UInt64.from(maxRows),
836
982
  })
837
983
  }
984
+
985
+ found(player: NameType, entityId: UInt64Type, location: CoordinatesType): Action {
986
+ return this.server.action('found', {
987
+ player: Name.from(player),
988
+ entity_id: UInt64.from(entityId),
989
+ x: Int64.from(location.x),
990
+ y: Int64.from(location.y),
991
+ })
992
+ }
993
+
994
+ naturalize(player: NameType, location: CoordinatesType): Action {
995
+ return this.server.action('naturalize', {
996
+ player: Name.from(player),
997
+ x: Int64.from(location.x),
998
+ y: Int64.from(location.y),
999
+ })
1000
+ }
1001
+
1002
+ contribute(
1003
+ player: NameType,
1004
+ entityId: UInt64Type,
1005
+ location: CoordinatesType,
1006
+ bundle: ServerContract.ActionParams.Type.cargo_item[]
1007
+ ): Action {
1008
+ return this.platform.action('contribute', {
1009
+ game: this.server.account,
1010
+ player: Name.from(player),
1011
+ entity_id: UInt64.from(entityId),
1012
+ x: Int64.from(location.x),
1013
+ y: Int64.from(location.y),
1014
+ bundle,
1015
+ })
1016
+ }
1017
+
1018
+ // addcontrib asserts the citizenship rows exist, so naturalize must precede contribute.
1019
+ firstContribution(
1020
+ player: NameType,
1021
+ entityId: UInt64Type,
1022
+ location: CoordinatesType,
1023
+ bundle: ServerContract.ActionParams.Type.cargo_item[]
1024
+ ): Action[] {
1025
+ return [
1026
+ this.naturalize(player, location),
1027
+ this.contribute(player, entityId, location, bundle),
1028
+ ]
1029
+ }
1030
+
1031
+ mintready(maxMints?: UInt32Type): Action {
1032
+ const params: {max_mints?: UInt32} = {}
1033
+ if (maxMints !== undefined) {
1034
+ params.max_mints = UInt32.from(maxMints)
1035
+ }
1036
+ return this.server.action('mintready', params)
1037
+ }
1038
+
1039
+ charterready(location: CoordinatesType): Action {
1040
+ return this.server.action('charterready', {
1041
+ x: Int64.from(location.x),
1042
+ y: Int64.from(location.y),
1043
+ })
1044
+ }
1045
+
1046
+ votecharter(player: NameType, location: CoordinatesType, nodeId: UInt32Type): Action {
1047
+ return this.server.action('votecharter', {
1048
+ player: Name.from(player),
1049
+ x: Int64.from(location.x),
1050
+ y: Int64.from(location.y),
1051
+ node_id: UInt32.from(nodeId),
1052
+ })
1053
+ }
1054
+
1055
+ voteready(maxBallots: UInt32Type = UInt32.from(0)): Action {
1056
+ return this.server.action('voteready', {
1057
+ max_ballots: UInt32.from(maxBallots),
1058
+ })
1059
+ }
838
1060
  }
@@ -0,0 +1,161 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {Asset, type Name} from '@wharfkit/antelope'
3
+ import {BalancesManager} from './balances'
4
+ import type {GameContext} from './context'
5
+ import {PlatformContract} from '../contracts'
6
+
7
+ interface StubOptions {
8
+ depositConfig?: {token_contract: string; token_symbol: string} | null
9
+ balances?: {token_contract: string; balance: string}[] | null
10
+ wallet?: string[]
11
+ }
12
+
13
+ function stubContext(options: StubOptions = {}) {
14
+ const counts = {depositcfg: 0, balance: 0, currency: 0}
15
+ const rows = (options.balances ?? []).map((row) => PlatformContract.Types.balance_row.from(row))
16
+ const platform = {
17
+ table(name: string) {
18
+ if (name === 'depositcfg') {
19
+ return {
20
+ get: async () => {
21
+ counts.depositcfg++
22
+ return options.depositConfig
23
+ ? PlatformContract.Types.depositcfg_row.from(options.depositConfig)
24
+ : undefined
25
+ },
26
+ }
27
+ }
28
+ return {
29
+ get: async (key: Name) => {
30
+ counts.balance++
31
+ return rows.find((row) => row.token_contract.equals(key))
32
+ },
33
+ all: async () => {
34
+ counts.balance++
35
+ return rows
36
+ },
37
+ }
38
+ },
39
+ }
40
+ const client = {
41
+ v1: {
42
+ chain: {
43
+ get_currency_balance: async () => {
44
+ counts.currency++
45
+ return (options.wallet ?? []).map((value) => Asset.from(value))
46
+ },
47
+ },
48
+ },
49
+ }
50
+ const context = {platform, client} as unknown as GameContext
51
+ return {manager: new BalancesManager(context), counts}
52
+ }
53
+
54
+ const SCRAP = {token_contract: 'scrap.gm', token_symbol: '0,SCRAP'}
55
+
56
+ describe('getDepositConfig', () => {
57
+ test('reads the configured deposit token', async () => {
58
+ const {manager} = stubContext({depositConfig: SCRAP})
59
+ const config = await manager.getDepositConfig()
60
+ expect(String(config?.tokenContract)).toBe('scrap.gm')
61
+ expect(String(config?.symbol)).toBe('0,SCRAP')
62
+ })
63
+
64
+ test('caches the config across calls', async () => {
65
+ const {manager, counts} = stubContext({depositConfig: SCRAP})
66
+ await manager.getDepositConfig()
67
+ await manager.getDepositConfig()
68
+ expect(counts.depositcfg).toBe(1)
69
+ await manager.getDepositConfig(true)
70
+ expect(counts.depositcfg).toBe(2)
71
+ })
72
+
73
+ test('caches a missing config as null without refetching', async () => {
74
+ const {manager, counts} = stubContext({depositConfig: null})
75
+ expect(await manager.getDepositConfig()).toBeNull()
76
+ expect(await manager.getDepositConfig()).toBeNull()
77
+ expect(counts.depositcfg).toBe(1)
78
+ })
79
+ })
80
+
81
+ describe('getPlatformBalance', () => {
82
+ test('returns null when the owner has never opened a row', async () => {
83
+ const {manager} = stubContext({depositConfig: SCRAP, balances: []})
84
+ expect(await manager.getPlatformBalance('alice')).toBeNull()
85
+ })
86
+
87
+ test('returns a zero balance for an opened but empty row', async () => {
88
+ const {manager} = stubContext({
89
+ depositConfig: SCRAP,
90
+ balances: [{token_contract: 'scrap.gm', balance: '0 SCRAP'}],
91
+ })
92
+ const balance = await manager.getPlatformBalance('alice')
93
+ expect(balance).not.toBeNull()
94
+ expect(String(balance?.balance)).toBe('0 SCRAP')
95
+ expect(balance?.balance.units.toNumber()).toBe(0)
96
+ expect(balance?.balance.symbol.precision).toBe(0)
97
+ expect(String(balance?.owner)).toBe('alice')
98
+ })
99
+
100
+ test('defaults to the deposit token and honours an explicit contract', async () => {
101
+ const {manager} = stubContext({
102
+ depositConfig: SCRAP,
103
+ balances: [
104
+ {token_contract: 'scrap.gm', balance: '12 SCRAP'},
105
+ {token_contract: 'eosio.token', balance: '3.0000 EOS'},
106
+ ],
107
+ })
108
+ expect(String((await manager.getPlatformBalance('alice'))?.balance)).toBe('12 SCRAP')
109
+ expect(String((await manager.getPlatformBalance('alice', 'eosio.token'))?.balance)).toBe(
110
+ '3.0000 EOS'
111
+ )
112
+ })
113
+
114
+ test('returns null when no deposit token is configured', async () => {
115
+ const {manager, counts} = stubContext({depositConfig: null})
116
+ expect(await manager.getPlatformBalance('alice')).toBeNull()
117
+ expect(counts.balance).toBe(0)
118
+ })
119
+ })
120
+
121
+ describe('getPlatformBalances', () => {
122
+ test('returns every row the owner holds', async () => {
123
+ const {manager} = stubContext({
124
+ depositConfig: SCRAP,
125
+ balances: [
126
+ {token_contract: 'scrap.gm', balance: '12 SCRAP'},
127
+ {token_contract: 'eosio.token', balance: '3.0000 EOS'},
128
+ ],
129
+ })
130
+ const balances = await manager.getPlatformBalances('alice')
131
+ expect(balances.map((row) => String(row.balance))).toEqual(['12 SCRAP', '3.0000 EOS'])
132
+ expect(balances.map((row) => String(row.tokenContract))).toEqual([
133
+ 'scrap.gm',
134
+ 'eosio.token',
135
+ ])
136
+ })
137
+
138
+ test('returns an empty array for an owner with no rows', async () => {
139
+ const {manager} = stubContext({depositConfig: SCRAP, balances: []})
140
+ expect(await manager.getPlatformBalances('alice')).toEqual([])
141
+ })
142
+ })
143
+
144
+ describe('getWalletBalance', () => {
145
+ test('reads the wallet balance for the deposit token', async () => {
146
+ const {manager} = stubContext({depositConfig: SCRAP, wallet: ['500 SCRAP']})
147
+ const balance = await manager.getWalletBalance('alice')
148
+ expect(String(balance?.balance)).toBe('500 SCRAP')
149
+ expect(String(balance?.tokenContract)).toBe('scrap.gm')
150
+ })
151
+
152
+ test('returns null when the account holds none of the token', async () => {
153
+ const {manager} = stubContext({depositConfig: SCRAP, wallet: []})
154
+ expect(await manager.getWalletBalance('alice')).toBeNull()
155
+ })
156
+
157
+ test('ignores balances of other symbols on the same contract', async () => {
158
+ const {manager} = stubContext({depositConfig: SCRAP, wallet: ['1.0000 OTHER']})
159
+ expect(await manager.getWalletBalance('alice')).toBeNull()
160
+ })
161
+ })
@@ -0,0 +1,93 @@
1
+ import {Asset, Name, type NameType} from '@wharfkit/antelope'
2
+ import {BaseManager} from './base'
3
+ import type {PlatformContract} from '../contracts'
4
+
5
+ export interface DepositConfig {
6
+ tokenContract: Name
7
+ symbol: Asset.Symbol
8
+ }
9
+
10
+ export interface PlatformBalance {
11
+ owner: Name
12
+ tokenContract: Name
13
+ balance: Asset
14
+ }
15
+
16
+ export interface TokenBalance {
17
+ account: Name
18
+ tokenContract: Name
19
+ balance: Asset
20
+ }
21
+
22
+ export class BalancesManager extends BaseManager {
23
+ private depositConfig?: DepositConfig | null
24
+
25
+ async getDepositConfig(reload = false): Promise<DepositConfig | null> {
26
+ if (!reload && this.depositConfig !== undefined) {
27
+ return this.depositConfig
28
+ }
29
+ const row = (await this.platform.table('depositcfg').get()) as
30
+ | PlatformContract.Types.depositcfg_row
31
+ | undefined
32
+ this.depositConfig = row
33
+ ? {tokenContract: row.token_contract, symbol: row.token_symbol}
34
+ : null
35
+ return this.depositConfig
36
+ }
37
+
38
+ private async resolveTokenContract(tokenContract?: NameType): Promise<Name | null> {
39
+ if (tokenContract) return Name.from(tokenContract)
40
+ const config = await this.getDepositConfig()
41
+ return config ? config.tokenContract : null
42
+ }
43
+
44
+ async getPlatformBalances(owner: NameType): Promise<PlatformBalance[]> {
45
+ const account = Name.from(owner)
46
+ const rows = (await this.platform
47
+ .table('balance', account)
48
+ .all()) as PlatformContract.Types.balance_row[]
49
+ return rows.map((row) => ({
50
+ owner: account,
51
+ tokenContract: row.token_contract,
52
+ balance: row.balance,
53
+ }))
54
+ }
55
+
56
+ // Null means no balance row at all: deposits abort until `open`. A zero row is a valid state.
57
+ async getPlatformBalance(
58
+ owner: NameType,
59
+ tokenContract?: NameType
60
+ ): Promise<PlatformBalance | null> {
61
+ const contract = await this.resolveTokenContract(tokenContract)
62
+ if (!contract) return null
63
+ const account = Name.from(owner)
64
+ const row = (await this.platform.table('balance', account).get(contract)) as
65
+ | PlatformContract.Types.balance_row
66
+ | undefined
67
+ if (!row) return null
68
+ return {owner: account, tokenContract: row.token_contract, balance: row.balance}
69
+ }
70
+
71
+ async getWalletBalance(
72
+ account: NameType,
73
+ tokenContract?: NameType,
74
+ symbolCode?: Asset.SymbolCodeType
75
+ ): Promise<TokenBalance | null> {
76
+ const contract = await this.resolveTokenContract(tokenContract)
77
+ if (!contract) return null
78
+ let code = symbolCode ? Asset.SymbolCode.from(symbolCode) : undefined
79
+ if (!code) {
80
+ const config = await this.getDepositConfig()
81
+ code = config?.symbol.code
82
+ }
83
+ const holder = Name.from(account)
84
+ const balances = await this.client.v1.chain.get_currency_balance(
85
+ contract,
86
+ holder,
87
+ code ? String(code) : undefined
88
+ )
89
+ const balance = balances.find((asset) => !code || asset.symbol.code.equals(code))
90
+ if (!balance) return null
91
+ return {account: holder, tokenContract: contract, balance}
92
+ }
93
+ }