@shipload/sdk 1.0.0-next.72 → 1.0.0-next.74

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 (47) hide show
  1. package/lib/shipload.d.ts +576 -133
  2. package/lib/shipload.js +6019 -5114
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +6002 -5108
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +194 -52
  7. package/lib/testing.js +767 -468
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +767 -468
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/contracts/server.ts +523 -143
  13. package/src/data/catalog.test.ts +81 -1
  14. package/src/data/catalog.ts +6 -4
  15. package/src/data/entities.json +2 -2
  16. package/src/data/metadata.ts +197 -363
  17. package/src/data/task-types.json +184 -0
  18. package/src/entities/makers.ts +2 -2
  19. package/src/index-module.ts +31 -2
  20. package/src/influence/civic-shuttle.test.ts +58 -0
  21. package/src/influence/civic-shuttle.ts +52 -6
  22. package/src/influence/duration.ts +0 -65
  23. package/src/influence/index.ts +4 -5
  24. package/src/influence/influence.test.ts +51 -136
  25. package/src/influence/valuation.ts +75 -0
  26. package/src/managers/actions.ts +49 -10
  27. package/src/managers/balances.test.ts +33 -0
  28. package/src/managers/balances.ts +15 -0
  29. package/src/managers/context.ts +50 -11
  30. package/src/managers/index.ts +14 -0
  31. package/src/managers/jobs.test.ts +82 -1
  32. package/src/managers/jobs.ts +41 -2
  33. package/src/managers/nft.test.ts +173 -1
  34. package/src/managers/nft.ts +35 -2
  35. package/src/managers/shuttle.test.ts +562 -0
  36. package/src/managers/shuttle.ts +397 -0
  37. package/src/resolution/resolve-item.ts +2 -2
  38. package/src/scheduling/availability.ts +10 -0
  39. package/src/scheduling/energy.ts +55 -1
  40. package/src/scheduling/jobs.test.ts +171 -1
  41. package/src/scheduling/jobs.ts +110 -13
  42. package/src/scheduling/projection.ts +2 -1
  43. package/src/scheduling/task-effects.ts +38 -0
  44. package/src/scheduling/unwrap.test.ts +60 -0
  45. package/src/scheduling/unwrap.ts +24 -8
  46. package/src/shipload.ts +23 -4
  47. package/src/types.ts +1 -0
@@ -41,7 +41,7 @@ describe('JobsManager.getOwnedJobs', () => {
41
41
  expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
42
42
  })
43
43
 
44
- it('reports an undeposited row as dropping off, whatever its window says', async () => {
44
+ it('reports a legacy undeposited row (inputs only) as dropping off, whatever its window says', async () => {
45
45
  const inFlight = row({
46
46
  deposited: false,
47
47
  cargo: [{item: 'in'}],
@@ -57,6 +57,27 @@ describe('JobsManager.getOwnedJobs', () => {
57
57
  expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
58
58
  })
59
59
 
60
+ it('reads a booking-time undeposited row whose input is booked as more than one stack', async () => {
61
+ const inFlight = row({
62
+ deposited: false,
63
+ cargo: [
64
+ {item_id: 101, quantity: 20},
65
+ {item_id: 101, quantity: 30},
66
+ ],
67
+ })
68
+ const m = managerWith(
69
+ async () => [inFlight],
70
+ async () => []
71
+ )
72
+ const jobs = await m.getOwnedJobs(OWNER, {now: new Date('2026-07-26T11:30:00Z')})
73
+ expect(jobs[0].status).toBe('dropping')
74
+ expect(jobs[0].inputs).toEqual([
75
+ {item_id: 101, quantity: 20},
76
+ {item_id: 101, quantity: 30},
77
+ ] as never)
78
+ expect(jobs[0].output).toBeNull()
79
+ })
80
+
60
81
  it('reports a cancelled row as ready with its inputs held', async () => {
61
82
  const cancelled = row({
62
83
  quantity: {toNumber: () => 0},
@@ -94,3 +115,63 @@ describe('JobsManager.getOwnedJobs', () => {
94
115
  expect(jobs).toHaveLength(1)
95
116
  })
96
117
  })
118
+
119
+ const buildRow = (over: Record<string, unknown> = {}) => ({
120
+ id: {toNumber: () => 9},
121
+ building: {toNumber: () => 2},
122
+ socket: {toNumber: () => 1},
123
+ target: {toNumber: () => 5},
124
+ owner: Name.from(OWNER),
125
+ coords: {x: {toNumber: () => 0}, y: {toNumber: () => 0}},
126
+ starts_at: {toDate: () => new Date('2026-07-26T10:00:00Z')},
127
+ completes_at: {toDate: () => new Date('2026-07-26T11:00:00Z')},
128
+ arrives_at: {toDate: () => new Date('2026-07-26T09:45:00Z')},
129
+ target_item_id: {toNumber: () => 10212},
130
+ cargo: [{item: 'in'}],
131
+ deposited: true,
132
+ ...over,
133
+ })
134
+
135
+ function managerWithReadonly(impl: (name: string, data: unknown) => Promise<unknown>) {
136
+ const ctx = {server: {readonly: impl}} as never
137
+ return new JobsManager(ctx)
138
+ }
139
+
140
+ describe('JobsManager.getBuildJobs', () => {
141
+ it('reads the dock queue through getbuildjobs and parses every row', async () => {
142
+ let called: unknown = null
143
+ const m = managerWithReadonly(async (name, data) => {
144
+ called = {name, data}
145
+ return {jobs: [buildRow(), buildRow({id: {toNumber: () => 10}, deposited: false})]}
146
+ })
147
+ const jobs = await m.getBuildJobs(2, {now: new Date('2026-07-26T11:30:00Z')})
148
+ expect(called).toMatchObject({name: 'getbuildjobs'})
149
+ expect(String((called as {data: {building_id: unknown}}).data.building_id)).toBe('2')
150
+ expect(jobs).toHaveLength(2)
151
+ expect(jobs[0]).toMatchObject({
152
+ id: 9,
153
+ building: 2,
154
+ socket: 1,
155
+ targetId: 5,
156
+ owner: OWNER,
157
+ targetItemId: 10212,
158
+ deposited: true,
159
+ status: 'ready',
160
+ })
161
+ expect(jobs[0].coords).toEqual({x: 0, y: 0})
162
+ expect(jobs[0].arrivesAt).toEqual(new Date('2026-07-26T09:45:00Z'))
163
+ expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
164
+ expect(jobs[1].status).toBe('dropping')
165
+ })
166
+
167
+ it('reports a booked row before its window as queued', async () => {
168
+ const m = managerWithReadonly(async () => ({jobs: [buildRow()]}))
169
+ const jobs = await m.getBuildJobs(2, {now: new Date('2026-07-26T09:50:00Z')})
170
+ expect(jobs[0].status).toBe('queued')
171
+ })
172
+
173
+ it('returns an empty list when the result carries no jobs', async () => {
174
+ const m = managerWithReadonly(async () => ({}))
175
+ expect(await m.getBuildJobs(2)).toEqual([])
176
+ })
177
+ })
@@ -1,9 +1,16 @@
1
- import {Name, type NameType} from '@wharfkit/antelope'
1
+ import {Name, UInt64, type NameType, type UInt64Type} from '@wharfkit/antelope'
2
2
  import {BaseManager} from './base'
3
- import {jobDeposited, jobStatus, splitJobCargo, type OwnedJob} from '../scheduling/jobs'
3
+ import {
4
+ jobDeposited,
5
+ jobStatus,
6
+ splitJobCargo,
7
+ type BuildJob,
8
+ type OwnedJob,
9
+ } from '../scheduling/jobs'
4
10
  import type {ServerContract} from '../contracts'
5
11
 
6
12
  type JobRow = ServerContract.Types.craftjob_row
13
+ type BuildJobRow = ServerContract.Types.buildjob_row
7
14
 
8
15
  export class JobsManager extends BaseManager {
9
16
  async getOwnedJobs(owner: NameType, opts?: {now?: Date}): Promise<OwnedJob[]> {
@@ -29,6 +36,38 @@ export class JobsManager extends BaseManager {
29
36
  return rows.filter((r) => ownerName.equals(r.owner)).map((r) => this.parseOwnedJob(r, now))
30
37
  }
31
38
 
39
+ async getBuildJobs(dockId: UInt64Type, opts?: {now?: Date}): Promise<BuildJob[]> {
40
+ const now = opts?.now ?? new Date()
41
+ const result = (await this.server.readonly('getbuildjobs', {
42
+ building_id: UInt64.from(dockId),
43
+ })) as ServerContract.Types.buildjobs_result | undefined
44
+ return (result?.jobs ?? []).map((r) => this.parseBuildJob(r, now))
45
+ }
46
+
47
+ private parseBuildJob(r: BuildJobRow, now: Date): BuildJob {
48
+ const startsAt = r.starts_at.toDate()
49
+ const completesAt = r.completes_at.toDate()
50
+ const arrivesAt = r.arrives_at.toDate()
51
+ const deposited = jobDeposited(r.deposited)
52
+ const building = r.building.toNumber()
53
+ const inputs = [...r.cargo]
54
+ return {
55
+ id: r.id.toNumber(),
56
+ building,
57
+ socket: r.socket.toNumber(),
58
+ targetId: r.target.toNumber(),
59
+ owner: r.owner.toString(),
60
+ coords: {x: r.coords.x.toNumber(), y: r.coords.y.toNumber()},
61
+ startsAt,
62
+ completesAt,
63
+ arrivesAt,
64
+ targetItemId: r.target_item_id.toNumber(),
65
+ status: jobStatus({startsAt, completesAt, arrivesAt, deposited, building, inputs}, now),
66
+ deposited,
67
+ inputs,
68
+ }
69
+ }
70
+
32
71
  private parseOwnedJob(r: JobRow, now: Date): OwnedJob {
33
72
  const startsAt = r.starts_at.toDate()
34
73
  const completesAt = r.completes_at.toDate()
@@ -1,5 +1,8 @@
1
1
  import {describe, expect, test} from 'bun:test'
2
- import {resolveLockedAmount, wrapCostKey} from './nft'
2
+ import {Asset, Name} from '@wharfkit/antelope'
3
+ import {NftManager, resolveLockedAmount, wrapCostKey} from './nft'
4
+ import type {GameContext} from './context'
5
+ import {ServerContract} from '../contracts'
3
6
 
4
7
  describe('resolveLockedAmount', () => {
5
8
  test('no fee refunds the full cost', () => {
@@ -19,3 +22,172 @@ describe('wrapCostKey', () => {
19
22
  expect(wrapCostKey(3, 2).toString()).toBe('770')
20
23
  })
21
24
  })
25
+
26
+ interface StubOptions {
27
+ wrapConfig?: {fee_pct: number; fee_account: string; min_asset_id?: number} | null
28
+ wrapCosts?: {item_type: number; tier: number; amount: number}[]
29
+ depositConfig?: {tokenContract: string; symbol: string} | null
30
+ }
31
+
32
+ function stubContext(options: StubOptions = {}) {
33
+ const counts = {wrapconfig: 0, wrapcost: 0, depositcfg: 0}
34
+ const costRows = (options.wrapCosts ?? []).map((row) =>
35
+ ServerContract.Types.wrapcost_row.from(row)
36
+ )
37
+ const server = {
38
+ table(name: string) {
39
+ if (name === 'wrapconfig') {
40
+ return {
41
+ get: async () => {
42
+ counts.wrapconfig++
43
+ return options.wrapConfig
44
+ ? ServerContract.Types.wrapconfig_row.from({
45
+ ...options.wrapConfig,
46
+ min_asset_id: options.wrapConfig.min_asset_id ?? 0,
47
+ })
48
+ : undefined
49
+ },
50
+ }
51
+ }
52
+ if (name === 'wrapcost') {
53
+ return {
54
+ get: async (key: {toString(): string}) => {
55
+ counts.wrapcost++
56
+ return costRows.find(
57
+ (row) =>
58
+ wrapCostKey(Number(row.item_type), Number(row.tier)).toString() ===
59
+ key.toString()
60
+ )
61
+ },
62
+ }
63
+ }
64
+ throw new Error(`unexpected table: ${name}`)
65
+ },
66
+ }
67
+ const balances = {
68
+ getDepositConfig: async () => {
69
+ counts.depositcfg++
70
+ return options.depositConfig
71
+ ? {
72
+ tokenContract: Name.from(options.depositConfig.tokenContract),
73
+ symbol: Asset.Symbol.from(options.depositConfig.symbol),
74
+ }
75
+ : null
76
+ },
77
+ }
78
+ const context = {server, balances} as unknown as GameContext
79
+ return {manager: new NftManager(context), counts}
80
+ }
81
+
82
+ const DEPOSIT = {tokenContract: 'scrap.gm', symbol: '0,SCRAP'}
83
+
84
+ describe('setWrapConfig', () => {
85
+ test('a seeded config keeps getWrapConfig off the chain', async () => {
86
+ const {manager, counts} = stubContext({wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'}})
87
+ manager.setWrapConfig({feePctBasisPoints: 200, feeAccount: 'fee.gm'})
88
+
89
+ const config = await manager.getWrapConfig()
90
+ expect(config?.feePctBasisPoints).toBe(200)
91
+ expect(String(config?.feeAccount)).toBe('fee.gm')
92
+ expect(counts.wrapconfig).toBe(0)
93
+ })
94
+
95
+ test('a reload still reaches the chain', async () => {
96
+ const {manager, counts} = stubContext({wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'}})
97
+ manager.setWrapConfig({feePctBasisPoints: 200, feeAccount: 'fee.gm'})
98
+ await manager.getWrapConfig(true)
99
+ expect(counts.wrapconfig).toBe(1)
100
+ })
101
+
102
+ test('seeding null records a chain with no wrap fee configured', async () => {
103
+ const {manager, counts} = stubContext({wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'}})
104
+ manager.setWrapConfig(null)
105
+ expect(await manager.getWrapConfig()).toBeNull()
106
+ expect(counts.wrapconfig).toBe(0)
107
+ })
108
+ })
109
+
110
+ describe('setWrapCosts', () => {
111
+ test('a seeded cost keeps getWrapCost off the chain', async () => {
112
+ const {manager, counts} = stubContext({
113
+ wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
114
+ })
115
+ manager.setWrapCosts([{itemType: 3, tier: 2, units: 500n}])
116
+
117
+ expect(await manager.getWrapCost(3, 2)).toBe(500n)
118
+ expect(counts.wrapcost).toBe(0)
119
+ })
120
+
121
+ test('an item missing from the seed falls back to chain', async () => {
122
+ const {manager, counts} = stubContext({
123
+ wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
124
+ })
125
+ manager.setWrapCosts([{itemType: 3, tier: 2, units: 500n}])
126
+
127
+ expect(await manager.getWrapCost(9, 1)).toBe(0n)
128
+ expect(counts.wrapcost).toBe(1)
129
+ })
130
+
131
+ test('a reload still reaches the chain', async () => {
132
+ const {manager, counts} = stubContext({
133
+ wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
134
+ })
135
+ manager.setWrapCosts([{itemType: 3, tier: 2, units: 500n}])
136
+ await manager.getWrapCost(3, 2, true)
137
+ expect(counts.wrapcost).toBe(1)
138
+ })
139
+ })
140
+
141
+ describe('getWrapDeposit', () => {
142
+ test('with every seed present, makes no chain reads', async () => {
143
+ const {manager, counts} = stubContext({
144
+ wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
145
+ wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'},
146
+ depositConfig: DEPOSIT,
147
+ })
148
+ manager.setWrapCosts([{itemType: 3, tier: 2, units: 500n}])
149
+ manager.setWrapConfig({feePctBasisPoints: 200, feeAccount: 'fee.gm'})
150
+
151
+ const deposit = await manager.getWrapDeposit(3, 2)
152
+ expect(deposit).toEqual({
153
+ cost: 500n,
154
+ refund: 490n,
155
+ feePct: 2,
156
+ symbol: 'SCRAP',
157
+ precision: 0,
158
+ tokenContract: 'scrap.gm',
159
+ })
160
+ expect(counts.wrapcost).toBe(0)
161
+ expect(counts.wrapconfig).toBe(0)
162
+ // getDepositConfig is a BalancesManager concern seeded separately; this stub always calls through.
163
+ expect(counts.depositcfg).toBe(1)
164
+ })
165
+
166
+ test('with nothing seeded, falls back to chain for config and cost', async () => {
167
+ const {manager, counts} = stubContext({
168
+ wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
169
+ wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'},
170
+ depositConfig: DEPOSIT,
171
+ })
172
+
173
+ const deposit = await manager.getWrapDeposit(3, 2)
174
+ expect(deposit?.cost).toBe(500n)
175
+ expect(counts.wrapcost).toBe(1)
176
+ expect(counts.wrapconfig).toBe(1)
177
+ })
178
+
179
+ test('reload true bypasses seeds and reaches chain', async () => {
180
+ const {manager, counts} = stubContext({
181
+ wrapCosts: [{item_type: 3, tier: 2, amount: 500}],
182
+ wrapConfig: {fee_pct: 200, fee_account: 'fee.gm'},
183
+ depositConfig: DEPOSIT,
184
+ })
185
+ manager.setWrapCosts([{itemType: 3, tier: 2, units: 500n}])
186
+ manager.setWrapConfig({feePctBasisPoints: 200, feeAccount: 'fee.gm'})
187
+
188
+ await manager.getWrapDeposit(3, 2, {reload: true})
189
+ expect(counts.wrapcost).toBe(1)
190
+ expect(counts.wrapconfig).toBe(1)
191
+ expect(counts.depositcfg).toBe(1)
192
+ })
193
+ })
@@ -12,6 +12,17 @@ export interface WrapConfig {
12
12
  feeAccount: Name
13
13
  }
14
14
 
15
+ export interface WrapConfigInput {
16
+ feePctBasisPoints: number
17
+ feeAccount: NameType
18
+ }
19
+
20
+ export interface WrapCostInput {
21
+ itemType: number
22
+ tier: number
23
+ units: bigint | string | number
24
+ }
25
+
15
26
  export interface WrapGate {
16
27
  owner: Name
17
28
  game: Name
@@ -39,6 +50,7 @@ export function wrapCostKey(itemType: number, tier: number): UInt64 {
39
50
  export class NftManager extends BaseManager {
40
51
  private cache = new Map<string, NftConfigForItem | null>()
41
52
  private wrapConfig?: WrapConfig | null
53
+ private wrapCosts?: Map<string, bigint> | null
42
54
 
43
55
  async getNftConfigForItem(itemId: UInt64Type): Promise<NftConfigForItem | undefined> {
44
56
  const id = UInt64.from(itemId)
@@ -56,6 +68,23 @@ export class NftManager extends BaseManager {
56
68
  return result ?? undefined
57
69
  }
58
70
 
71
+ setWrapConfig(config: WrapConfigInput | null): void {
72
+ this.wrapConfig = config
73
+ ? {
74
+ feePctBasisPoints: config.feePctBasisPoints,
75
+ feeAccount: Name.from(config.feeAccount),
76
+ }
77
+ : null
78
+ }
79
+
80
+ setWrapCosts(costs: WrapCostInput[] | null): void {
81
+ this.wrapCosts = costs
82
+ ? new Map(
83
+ costs.map((c) => [wrapCostKey(c.itemType, c.tier).toString(), BigInt(c.units)])
84
+ )
85
+ : null
86
+ }
87
+
59
88
  async getWrapConfig(reload = false): Promise<WrapConfig | null> {
60
89
  if (!reload && this.wrapConfig !== undefined) {
61
90
  return this.wrapConfig
@@ -69,7 +98,11 @@ export class NftManager extends BaseManager {
69
98
  return this.wrapConfig
70
99
  }
71
100
 
72
- async getWrapCost(itemType: number, tier: number): Promise<bigint> {
101
+ async getWrapCost(itemType: number, tier: number, reload = false): Promise<bigint> {
102
+ if (!reload && this.wrapCosts) {
103
+ const seeded = this.wrapCosts.get(wrapCostKey(itemType, tier).toString())
104
+ if (seeded !== undefined) return seeded
105
+ }
73
106
  const row = (await this.server.table('wrapcost').get(wrapCostKey(itemType, tier))) as
74
107
  | ServerContract.Types.wrapcost_row
75
108
  | undefined
@@ -81,7 +114,7 @@ export class NftManager extends BaseManager {
81
114
  tier: number,
82
115
  opts: {reload?: boolean} = {}
83
116
  ): Promise<WrapDeposit | null> {
84
- const cost = await this.getWrapCost(itemType, tier)
117
+ const cost = await this.getWrapCost(itemType, tier, opts.reload)
85
118
  if (cost === 0n) return null
86
119
 
87
120
  const wrapConfig = await this.getWrapConfig(opts.reload)