@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.
- package/lib/shipload.d.ts +1829 -501
- package/lib/shipload.js +4325 -1324
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +4286 -1389
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +391 -13
- package/lib/testing.js +1692 -107
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +1693 -108
- package/lib/testing.m.js.map +1 -1
- package/package.json +2 -2
- package/src/contracts/fund.ts +201 -0
- package/src/contracts/platform.ts +73 -1
- package/src/contracts/server.ts +946 -26
- package/src/contracts/token.ts +170 -0
- package/src/contracts.ts +2 -0
- package/src/data/catalog.test.ts +19 -0
- package/src/data/catalog.ts +6 -0
- package/src/data/charters.json +198 -0
- package/src/data/citizenry-adjectives.json +42 -0
- package/src/data/citizenry-nouns.json +50 -0
- package/src/data/entities.json +36 -0
- package/src/data/item-ids.ts +2 -0
- package/src/data/items.json +12 -0
- package/src/data/kind-registry.json +20 -3
- package/src/data/kind-registry.ts +5 -0
- package/src/data/metadata.ts +14 -0
- package/src/data/recipes.json +0 -48
- package/src/derivation/capabilities.ts +14 -0
- package/src/derivation/recipe-usage.test.ts +0 -2
- package/src/format.ts +9 -0
- package/src/index-module.ts +32 -2
- package/src/influence/categories.ts +32 -0
- package/src/influence/charters.test.ts +126 -0
- package/src/influence/charters.ts +120 -0
- package/src/influence/citizenry.ts +38 -0
- package/src/influence/constants.ts +63 -0
- package/src/influence/decay.ts +20 -0
- package/src/influence/decomp.ts +95 -0
- package/src/influence/demand.ts +106 -0
- package/src/influence/duration.ts +36 -0
- package/src/influence/index.ts +63 -0
- package/src/influence/influence.test.ts +317 -0
- package/src/influence/quality.ts +25 -0
- package/src/influence/valuation.ts +154 -0
- package/src/managers/actions.ts +227 -5
- package/src/managers/balances.test.ts +161 -0
- package/src/managers/balances.ts +93 -0
- package/src/managers/construction.ts +11 -1
- package/src/managers/context.ts +18 -0
- package/src/managers/entities.ts +11 -0
- package/src/managers/index.ts +15 -1
- package/src/managers/influence.test.ts +58 -0
- package/src/managers/influence.ts +347 -0
- package/src/managers/nft.test.ts +8 -1
- package/src/managers/nft.ts +56 -16
- package/src/nft/description.ts +7 -0
- package/src/scheduling/task-cargo.ts +1 -0
- package/src/shipload.ts +10 -0
- package/src/types.ts +23 -4
- package/src/utils/hash.ts +4 -0
- 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
|
+
}
|
package/src/managers/actions.ts
CHANGED
|
@@ -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
|
-
|
|
659
|
-
|
|
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
|
-
|
|
694
|
-
|
|
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
|
+
}
|