@aephia/atlas-kit 0.4.0 → 0.6.0-next.1
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/README.md +49 -29
- package/dist/cargo/index.d.ts +1 -0
- package/dist/cargo/index.d.ts.map +1 -1
- package/dist/cargo/index.js +1 -0
- package/dist/cargo/index.js.map +1 -1
- package/dist/claim-stakes/actions/index.d.ts +393 -0
- package/dist/claim-stakes/actions/index.d.ts.map +1 -0
- package/dist/claim-stakes/actions/index.js +1747 -0
- package/dist/claim-stakes/actions/index.js.map +1 -0
- package/dist/claim-stakes/index.d.ts +64 -0
- package/dist/claim-stakes/index.d.ts.map +1 -1
- package/dist/claim-stakes/index.js +49 -0
- package/dist/claim-stakes/index.js.map +1 -1
- package/dist/client/context.d.ts.map +1 -1
- package/dist/client/context.js +2 -1
- package/dist/client/context.js.map +1 -1
- package/dist/client/errors.d.ts +12 -3
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +11 -0
- package/dist/client/errors.js.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/lifecycle.d.ts +2 -0
- package/dist/client/lifecycle.d.ts.map +1 -1
- package/dist/client/lifecycle.js +31 -0
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/combat/actions/index.d.ts +239 -0
- package/dist/combat/actions/index.d.ts.map +1 -0
- package/dist/combat/actions/index.js +1146 -0
- package/dist/combat/actions/index.js.map +1 -0
- package/dist/crafting/actions/index.d.ts +140 -0
- package/dist/crafting/actions/index.d.ts.map +1 -0
- package/dist/crafting/actions/index.js +728 -0
- package/dist/crafting/actions/index.js.map +1 -0
- package/dist/crafting/index.d.ts +74 -8
- package/dist/crafting/index.d.ts.map +1 -1
- package/dist/crafting/index.js +55 -9
- package/dist/crafting/index.js.map +1 -1
- package/dist/fleets/actions/index.d.ts +5 -4
- package/dist/fleets/actions/index.d.ts.map +1 -1
- package/dist/fleets/actions/index.js +85 -58
- package/dist/fleets/actions/index.js.map +1 -1
- package/dist/fleets/index.d.ts +1 -0
- package/dist/fleets/index.d.ts.map +1 -1
- package/dist/fleets/index.js +1 -0
- package/dist/fleets/index.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/c4/claim-stake-building-definitions.d.ts +50 -0
- package/dist/internal/c4/claim-stake-building-definitions.d.ts.map +1 -0
- package/dist/internal/c4/claim-stake-building-definitions.js +56 -0
- package/dist/internal/c4/claim-stake-building-definitions.js.map +1 -0
- package/dist/internal/c4/claim-stake-configuration.d.ts +17 -0
- package/dist/internal/c4/claim-stake-configuration.d.ts.map +1 -0
- package/dist/internal/c4/claim-stake-configuration.js +47 -0
- package/dist/internal/c4/claim-stake-configuration.js.map +1 -0
- package/dist/internal/c4/claim-stake-definitions.d.ts +37 -0
- package/dist/internal/c4/claim-stake-definitions.d.ts.map +1 -0
- package/dist/internal/c4/claim-stake-definitions.js +77 -0
- package/dist/internal/c4/claim-stake-definitions.js.map +1 -0
- package/dist/internal/c4/fleet.d.ts.map +1 -1
- package/dist/internal/c4/fleet.js +2 -102
- package/dist/internal/c4/fleet.js.map +1 -1
- package/dist/internal/c4/game-definition-layout.d.ts +11 -0
- package/dist/internal/c4/game-definition-layout.d.ts.map +1 -0
- package/dist/internal/c4/game-definition-layout.js +32 -0
- package/dist/internal/c4/game-definition-layout.js.map +1 -0
- package/dist/internal/c4/game.d.ts +13 -1
- package/dist/internal/c4/game.d.ts.map +1 -1
- package/dist/internal/c4/game.js +19 -37
- package/dist/internal/c4/game.js.map +1 -1
- package/dist/internal/c4/profile-faction.d.ts +6 -0
- package/dist/internal/c4/profile-faction.d.ts.map +1 -1
- package/dist/internal/c4/profile-faction.js +22 -1
- package/dist/internal/c4/profile-faction.js.map +1 -1
- package/dist/internal/c4/ship-stats.d.ts +5 -0
- package/dist/internal/c4/ship-stats.d.ts.map +1 -0
- package/dist/internal/c4/ship-stats.js +106 -0
- package/dist/internal/c4/ship-stats.js.map +1 -0
- package/dist/internal/c4/world.d.ts +23 -1
- package/dist/internal/c4/world.d.ts.map +1 -1
- package/dist/internal/c4/world.js +53 -1
- package/dist/internal/c4/world.js.map +1 -1
- package/dist/internal/crafting-process-state.d.ts +15 -0
- package/dist/internal/crafting-process-state.d.ts.map +1 -0
- package/dist/internal/crafting-process-state.js +12 -0
- package/dist/internal/crafting-process-state.js.map +1 -0
- package/dist/internal/planning/authorization-adapter.d.ts +74 -0
- package/dist/internal/planning/authorization-adapter.d.ts.map +1 -1
- package/dist/internal/planning/authorization-adapter.js +382 -0
- package/dist/internal/planning/authorization-adapter.js.map +1 -1
- package/dist/internal/read-meta.d.ts +39 -3
- package/dist/internal/read-meta.d.ts.map +1 -1
- package/dist/internal/read-meta.js +0 -10
- package/dist/internal/read-meta.js.map +1 -1
- package/dist/loyalty/actions/index.d.ts +40 -0
- package/dist/loyalty/actions/index.d.ts.map +1 -0
- package/dist/loyalty/actions/index.js +159 -0
- package/dist/loyalty/actions/index.js.map +1 -0
- package/dist/markets/actions/index.d.ts +100 -0
- package/dist/markets/actions/index.d.ts.map +1 -0
- package/dist/markets/actions/index.js +417 -0
- package/dist/markets/actions/index.js.map +1 -0
- package/dist/planning/assembly.d.ts +4 -0
- package/dist/planning/assembly.d.ts.map +1 -1
- package/dist/planning/assembly.js +13 -0
- package/dist/planning/assembly.js.map +1 -1
- package/dist/planning/composition.d.ts.map +1 -1
- package/dist/planning/composition.js +21 -2
- package/dist/planning/composition.js.map +1 -1
- package/dist/planning/execution.d.ts.map +1 -1
- package/dist/planning/execution.js +7 -4
- package/dist/planning/execution.js.map +1 -1
- package/dist/planning/index.d.ts +27 -8
- package/dist/planning/index.d.ts.map +1 -1
- package/dist/planning/index.js +57 -13
- package/dist/planning/index.js.map +1 -1
- package/dist/planning/plan-sequence-execution.d.ts +26 -4
- package/dist/planning/plan-sequence-execution.d.ts.map +1 -1
- package/dist/planning/plan-sequence-execution.js +26 -4
- package/dist/planning/plan-sequence-execution.js.map +1 -1
- package/dist/planning/plan-sequence.d.ts +20 -3
- package/dist/planning/plan-sequence.d.ts.map +1 -1
- package/dist/planning/plan-sequence.js +20 -3
- package/dist/planning/plan-sequence.js.map +1 -1
- package/dist/planning/simulation.d.ts.map +1 -1
- package/dist/planning/simulation.js +2 -1
- package/dist/planning/simulation.js.map +1 -1
- package/dist/registry/cargo.d.ts +10 -0
- package/dist/registry/cargo.d.ts.map +1 -0
- package/dist/registry/cargo.js +15 -0
- package/dist/registry/cargo.js.map +1 -0
- package/dist/registry/claim-stakes.d.ts +11 -0
- package/dist/registry/claim-stakes.d.ts.map +1 -0
- package/dist/registry/claim-stakes.js +15 -0
- package/dist/registry/claim-stakes.js.map +1 -0
- package/dist/registry/definition-section-snapshot.d.ts +42 -0
- package/dist/registry/definition-section-snapshot.d.ts.map +1 -0
- package/dist/registry/definition-section-snapshot.js +214 -0
- package/dist/registry/definition-section-snapshot.js.map +1 -0
- package/dist/registry/index.d.ts +114 -7
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +372 -101
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/publication.d.ts +8 -0
- package/dist/registry/publication.d.ts.map +1 -0
- package/dist/registry/publication.js +10 -0
- package/dist/registry/publication.js.map +1 -0
- package/dist/registry/ship-stats-snapshot.d.ts +8 -0
- package/dist/registry/ship-stats-snapshot.d.ts.map +1 -0
- package/dist/registry/ship-stats-snapshot.js +154 -0
- package/dist/registry/ship-stats-snapshot.js.map +1 -0
- package/dist/world/index.d.ts +22 -1
- package/dist/world/index.d.ts.map +1 -1
- package/dist/world/index.js +24 -1
- package/dist/world/index.js.map +1 -1
- package/package.json +21 -1
|
@@ -0,0 +1,1747 @@
|
|
|
1
|
+
import { getAddressEncoder, getBytesEncoder, getProgramDerivedAddress, isAddress, } from '@solana/kit';
|
|
2
|
+
import { InvalidActionInputError, SageSdkError } from '../../client/errors.js';
|
|
3
|
+
import { readContextRawAccounts } from '../../client/reconciliation.js';
|
|
4
|
+
import { sage } from '../../internal/c4/bindings.js';
|
|
5
|
+
import { readFreshGameClaimStakeConfiguration, } from '../../registry/claim-stakes.js';
|
|
6
|
+
import { characterAccountDefinition, findCharacterPda, } from '../../internal/c4/identity.js';
|
|
7
|
+
import { deriveFactionOwnershipAddress, factionOwnershipAccountDefinition, } from '../../internal/c4/factions.js';
|
|
8
|
+
import { claimStakeAccountDefinition } from '../../internal/c4/claim-stake.js';
|
|
9
|
+
import { deriveFleetCrewBindingAddress, fleetCrewBindingAccountDefinition, } from '../../internal/c4/fleet-crew-binding.js';
|
|
10
|
+
import { deriveFleetAddress, fleetAccountDefinition, } from '../../internal/c4/fleet.js';
|
|
11
|
+
import { deriveProfileFactionAddress, profileFactionIdentityAccountDefinition, } from '../../internal/c4/profile-faction.js';
|
|
12
|
+
import { deriveStarbasePlayerAddress, starbasePlayerAccountDefinition, } from '../../internal/c4/starbase.js';
|
|
13
|
+
import { celestialBodyAccountDefinition, decodeIndexedCelestialBodyAccount, deriveCelestialBodyAddress, deriveStarSystemAddress, starSystemAccountDefinition, } from '../../internal/c4/world.js';
|
|
14
|
+
import { requireGameAddress } from '../../internal/game-context.js';
|
|
15
|
+
import { adaptCancelClaimStakeBuildingChangesInstruction, adaptClaimStakeBuildingChangesInstruction, adaptDeconstructClaimStakeInstruction, adaptFinalizeClaimStakeBuildingChangesInstruction, adaptPlaceClaimStakeInstruction, adaptRecoverClaimStakeFleetTransferInstruction, adaptStartClaimStakeFleetTransferInstruction, adaptTopUpClaimStakeRentInstruction, validatePlanAuthorization, } from '../../internal/planning/authorization-adapter.js';
|
|
16
|
+
import { createPlan, } from '../../planning/index.js';
|
|
17
|
+
import { readFreshGameCargoDefinitions } from '../../registry/cargo.js';
|
|
18
|
+
import { resolveCargo } from '../../registry/index.js';
|
|
19
|
+
const U64_MAX = (1n << 64n) - 1n;
|
|
20
|
+
const U16_MAX = 65_535;
|
|
21
|
+
const I32_MAX = 2_147_483_647;
|
|
22
|
+
const I32_MIN_MAGNITUDE = 2_147_483_648;
|
|
23
|
+
const U32_MAX = 4_294_967_295;
|
|
24
|
+
const LEVEL_INDEX = Object.freeze({
|
|
25
|
+
'level-0': 0,
|
|
26
|
+
'level-1': 1,
|
|
27
|
+
'level-2': 2,
|
|
28
|
+
'level-3': 3,
|
|
29
|
+
'level-4': 4,
|
|
30
|
+
'level-5': 5,
|
|
31
|
+
css: 6,
|
|
32
|
+
});
|
|
33
|
+
const CLAIM_STAKE_PLOT_LEVELS = Object.freeze([
|
|
34
|
+
sage.StarbaseLevel.Level0,
|
|
35
|
+
sage.StarbaseLevel.Level1,
|
|
36
|
+
sage.StarbaseLevel.Level2,
|
|
37
|
+
sage.StarbaseLevel.Level3,
|
|
38
|
+
sage.StarbaseLevel.Level4,
|
|
39
|
+
sage.StarbaseLevel.Level5,
|
|
40
|
+
sage.StarbaseLevel.Css,
|
|
41
|
+
]);
|
|
42
|
+
const TIER_INDEX = Object.freeze({
|
|
43
|
+
one: 0,
|
|
44
|
+
two: 1,
|
|
45
|
+
three: 2,
|
|
46
|
+
four: 3,
|
|
47
|
+
five: 4,
|
|
48
|
+
});
|
|
49
|
+
function invalid(field, value, acceptedRange, remedy) {
|
|
50
|
+
throw new InvalidActionInputError(field, value, acceptedRange, remedy);
|
|
51
|
+
}
|
|
52
|
+
function fail(message, address) {
|
|
53
|
+
throw new SageSdkError('ACTION_PRECONDITION_FAILED', message, { address });
|
|
54
|
+
}
|
|
55
|
+
function record(value, field) {
|
|
56
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
57
|
+
return invalid(field, value, 'a plain object', `Provide ${field} as the documented object before retrying.`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function exactKeys(value, field, required, optional = []) {
|
|
62
|
+
const accepted = new Set([...required, ...optional]);
|
|
63
|
+
const missing = required.find((key) => !Object.hasOwn(value, key));
|
|
64
|
+
const unexpected = Object.keys(value).find((key) => !accepted.has(key));
|
|
65
|
+
if (missing !== undefined || unexpected !== undefined) {
|
|
66
|
+
invalid(field, missing ?? unexpected, `exactly the fields ${[...required, ...optional].join(', ')}`, `Remove unknown fields and provide every required ${field} field before retrying.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function address(value, field) {
|
|
70
|
+
if (typeof value !== 'string' || !isAddress(value)) {
|
|
71
|
+
return invalid(field, value, 'one valid z.ink/Solana account address', `Set ${field} to the documented public account address before retrying.`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function u16(value, field) {
|
|
76
|
+
if (typeof value !== 'number' ||
|
|
77
|
+
!Number.isInteger(value) ||
|
|
78
|
+
value < 0 ||
|
|
79
|
+
value > U16_MAX) {
|
|
80
|
+
return invalid(field, value, `an integer from 0 through ${String(U16_MAX)} (unsigned u16)`, `Set ${field} to an exact id from the configured Game catalog before retrying.`);
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function validateOptions(value) {
|
|
85
|
+
const options = record(value, 'options');
|
|
86
|
+
exactKeys(options, 'options', [
|
|
87
|
+
'authorization',
|
|
88
|
+
'claimStake',
|
|
89
|
+
'claimStakeDefinitionId',
|
|
90
|
+
'hubBuildingId',
|
|
91
|
+
'initialRentAmount',
|
|
92
|
+
]);
|
|
93
|
+
const authorizationRecord = record(options.authorization, 'authorization');
|
|
94
|
+
exactKeys(authorizationRecord, 'authorization', ['profile', 'authority', 'keyIndex'], ['certificate']);
|
|
95
|
+
const authorization = authorizationRecord;
|
|
96
|
+
address(authorization.profile, 'authorization.profile');
|
|
97
|
+
address(authorization.authority, 'authorization.authority');
|
|
98
|
+
if (authorization.certificate !== undefined) {
|
|
99
|
+
address(authorization.certificate, 'authorization.certificate');
|
|
100
|
+
}
|
|
101
|
+
validatePlanAuthorization(authorization);
|
|
102
|
+
if (typeof options.initialRentAmount !== 'bigint' ||
|
|
103
|
+
options.initialRentAmount <= 0n ||
|
|
104
|
+
options.initialRentAmount > U64_MAX) {
|
|
105
|
+
invalid('initialRentAmount', options.initialRentAmount, `a positive raw bigint from 1 through ${String(U64_MAX)} (unsigned u64)`, 'Set initialRentAmount to the exact positive raw rent deposit within the instruction codec range.');
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
authorization,
|
|
109
|
+
claimStake: address(options.claimStake, 'claimStake'),
|
|
110
|
+
claimStakeDefinitionId: u16(options.claimStakeDefinitionId, 'claimStakeDefinitionId'),
|
|
111
|
+
hubBuildingId: u16(options.hubBuildingId, 'hubBuildingId'),
|
|
112
|
+
initialRentAmount: options.initialRentAmount,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function validateAuthorization(value) {
|
|
116
|
+
const authorizationRecord = record(value, 'authorization');
|
|
117
|
+
exactKeys(authorizationRecord, 'authorization', ['profile', 'authority', 'keyIndex'], ['certificate']);
|
|
118
|
+
const authorization = authorizationRecord;
|
|
119
|
+
address(authorization.profile, 'authorization.profile');
|
|
120
|
+
address(authorization.authority, 'authorization.authority');
|
|
121
|
+
if (authorization.certificate !== undefined) {
|
|
122
|
+
address(authorization.certificate, 'authorization.certificate');
|
|
123
|
+
}
|
|
124
|
+
validatePlanAuthorization(authorization);
|
|
125
|
+
return authorization;
|
|
126
|
+
}
|
|
127
|
+
function validateBuildingChangeOptions(value) {
|
|
128
|
+
const options = record(value, 'options');
|
|
129
|
+
exactKeys(options, 'options', ['authorization', 'buildingChanges']);
|
|
130
|
+
const authorization = validateAuthorization(options.authorization);
|
|
131
|
+
const values = options.buildingChanges;
|
|
132
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
133
|
+
invalid('buildingChanges', values, 'a non-empty dense array of add/remove building changes', 'Provide at least one exact building change before retrying.');
|
|
134
|
+
}
|
|
135
|
+
const changes = [];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
138
|
+
if (!Object.hasOwn(values, index)) {
|
|
139
|
+
invalid('buildingChanges', index, 'a dense array without missing elements', 'Remove array holes and provide each building change explicitly before retrying.');
|
|
140
|
+
}
|
|
141
|
+
const field = `buildingChanges[${String(index)}]`;
|
|
142
|
+
const change = record(values[index], field);
|
|
143
|
+
exactKeys(change, field, ['buildingId', 'kind', 'quantity']);
|
|
144
|
+
const buildingId = u16(change.buildingId, `${field}.buildingId`);
|
|
145
|
+
if (change.kind !== 'add' && change.kind !== 'remove') {
|
|
146
|
+
invalid(`${field}.kind`, change.kind, 'add or remove', `Set ${field}.kind to the intended building operation before retrying.`);
|
|
147
|
+
}
|
|
148
|
+
const maximumQuantity = change.kind === 'remove' ? I32_MIN_MAGNITUDE : I32_MAX;
|
|
149
|
+
if (typeof change.quantity !== 'number' ||
|
|
150
|
+
!Number.isInteger(change.quantity) ||
|
|
151
|
+
!Number.isFinite(change.quantity) ||
|
|
152
|
+
change.quantity < 1 ||
|
|
153
|
+
change.quantity > maximumQuantity) {
|
|
154
|
+
invalid(`${field}.quantity`, change.quantity, `a positive integer from 1 through ${String(maximumQuantity)}`, `Set ${field}.quantity to an exact positive instruction quantity before retrying.`);
|
|
155
|
+
}
|
|
156
|
+
if (seen.has(buildingId)) {
|
|
157
|
+
invalid(`${field}.buildingId`, buildingId, 'one unique building id per change list', 'Combine duplicate building changes into one add or remove entry before retrying.');
|
|
158
|
+
}
|
|
159
|
+
seen.add(buildingId);
|
|
160
|
+
changes.push({
|
|
161
|
+
buildingId,
|
|
162
|
+
kind: change.kind,
|
|
163
|
+
quantity: change.quantity,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return { authorization, buildingChanges: changes };
|
|
167
|
+
}
|
|
168
|
+
function validateFinalizeBuildingChangeOptions(value) {
|
|
169
|
+
const options = record(value, 'options');
|
|
170
|
+
exactKeys(options, 'options', ['authorization']);
|
|
171
|
+
return { authorization: validateAuthorization(options.authorization) };
|
|
172
|
+
}
|
|
173
|
+
function validateCancelBuildingChangeOptions(value) {
|
|
174
|
+
const options = record(value, 'options');
|
|
175
|
+
exactKeys(options, 'options', ['authorization']);
|
|
176
|
+
return { authorization: validateAuthorization(options.authorization) };
|
|
177
|
+
}
|
|
178
|
+
function validateDeconstructOptions(value) {
|
|
179
|
+
const options = record(value, 'options');
|
|
180
|
+
exactKeys(options, 'options', ['authorization']);
|
|
181
|
+
return { authorization: validateAuthorization(options.authorization) };
|
|
182
|
+
}
|
|
183
|
+
function validateTopUpRentOptions(value) {
|
|
184
|
+
const options = record(value, 'options');
|
|
185
|
+
exactKeys(options, 'options', ['amount', 'authorization']);
|
|
186
|
+
const authorization = validateAuthorization(options.authorization);
|
|
187
|
+
if (typeof options.amount !== 'bigint' ||
|
|
188
|
+
options.amount <= 0n ||
|
|
189
|
+
options.amount > U64_MAX) {
|
|
190
|
+
invalid('amount', options.amount, `a positive raw bigint from 1 through ${String(U64_MAX)} (unsigned u64)`, 'Set amount to the exact positive raw rent top-up within the instruction codec range.');
|
|
191
|
+
}
|
|
192
|
+
return { amount: options.amount, authorization };
|
|
193
|
+
}
|
|
194
|
+
function validateCargoRequests(value, field, seen) {
|
|
195
|
+
if (!Array.isArray(value)) {
|
|
196
|
+
invalid(field, value, 'a dense array of exact raw cargo requests', `Provide ${field} as an array, using an empty array when that direction has no cargo.`);
|
|
197
|
+
}
|
|
198
|
+
const requests = [];
|
|
199
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
200
|
+
const itemField = `${field}[${String(index)}]`;
|
|
201
|
+
if (!Object.hasOwn(value, index)) {
|
|
202
|
+
invalid(itemField, undefined, 'a present cargo request', `Remove sparse entries from ${field} before retrying.`);
|
|
203
|
+
}
|
|
204
|
+
const item = record(value[index], itemField);
|
|
205
|
+
exactKeys(item, itemField, ['cargoId', 'amount']);
|
|
206
|
+
const cargoId = u16(item.cargoId, `${itemField}.cargoId`);
|
|
207
|
+
if (seen.has(cargoId)) {
|
|
208
|
+
invalid(`${itemField}.cargoId`, cargoId, 'one unique cargo id across load and unload', 'Combine duplicate quantities into one direction or remove the cancelling request before retrying.');
|
|
209
|
+
}
|
|
210
|
+
if (typeof item.amount !== 'bigint' ||
|
|
211
|
+
item.amount <= 0n ||
|
|
212
|
+
item.amount > U64_MAX) {
|
|
213
|
+
invalid(`${itemField}.amount`, item.amount, `a positive raw bigint from 1 through ${String(U64_MAX)} (unsigned u64)`, `Set ${itemField}.amount to an exact positive raw-unit quantity within the instruction codec range.`);
|
|
214
|
+
}
|
|
215
|
+
seen.add(cargoId);
|
|
216
|
+
requests.push({ amount: item.amount, cargoId });
|
|
217
|
+
}
|
|
218
|
+
return Object.freeze(requests);
|
|
219
|
+
}
|
|
220
|
+
function validateStartFleetTransferOptions(value) {
|
|
221
|
+
const options = record(value, 'options');
|
|
222
|
+
exactKeys(options, 'options', ['authorization', 'load', 'unload']);
|
|
223
|
+
const authorization = validateAuthorization(options.authorization);
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
const load = validateCargoRequests(options.load, 'load', seen);
|
|
226
|
+
const unload = validateCargoRequests(options.unload, 'unload', seen);
|
|
227
|
+
if (load.length === 0 && unload.length === 0) {
|
|
228
|
+
invalid('load/unload', 'both empty', 'at least one exact load or unload request', 'Add cargo to load from the Claim Stake or unload from the Fleet before retrying.');
|
|
229
|
+
}
|
|
230
|
+
return { authorization, load, unload };
|
|
231
|
+
}
|
|
232
|
+
function validateRecoverFleetTransferOptions(value) {
|
|
233
|
+
const options = record(value, 'options');
|
|
234
|
+
exactKeys(options, 'options', ['atUnixSeconds', 'authorization']);
|
|
235
|
+
const authorization = validateAuthorization(options.authorization);
|
|
236
|
+
if (typeof options.atUnixSeconds !== 'bigint' ||
|
|
237
|
+
options.atUnixSeconds < 0n ||
|
|
238
|
+
options.atUnixSeconds > U64_MAX) {
|
|
239
|
+
invalid('atUnixSeconds', options.atUnixSeconds, `a non-negative bigint through ${String(U64_MAX)} (unsigned u64 Unix seconds)`, 'Set atUnixSeconds to the explicit current Unix timestamp before retrying recovery.');
|
|
240
|
+
}
|
|
241
|
+
return { atUnixSeconds: options.atUnixSeconds, authorization };
|
|
242
|
+
}
|
|
243
|
+
async function deriveCurrencyCacheAddress(game) {
|
|
244
|
+
const [derived] = await getProgramDerivedAddress({
|
|
245
|
+
programAddress: sage.SAGE_PROGRAM_ADDRESS,
|
|
246
|
+
seeds: [
|
|
247
|
+
getBytesEncoder().encode(new TextEncoder().encode('CurrencyConfig')),
|
|
248
|
+
getAddressEncoder().encode(game),
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
return derived;
|
|
252
|
+
}
|
|
253
|
+
function validateCurrencyCache(account, game, canonicalAddress, action) {
|
|
254
|
+
const decoder = sage.getCurrencyConfigCacheDecoder();
|
|
255
|
+
const validDiscriminator = sage.CURRENCY_CONFIG_CACHE_DISCRIMINATOR.every((byte, index) => account.data[index] === byte);
|
|
256
|
+
if (account.address !== canonicalAddress ||
|
|
257
|
+
account.owner !== sage.SAGE_PROGRAM_ADDRESS ||
|
|
258
|
+
account.data.length !== decoder.fixedSize ||
|
|
259
|
+
!validDiscriminator ||
|
|
260
|
+
decoder.decode(account.data).gameId !== game) {
|
|
261
|
+
fail(`CurrencyCache ${account.address} does not match the configured Game, canonical PDA, owner, or pinned account layout. Refresh the Game currency configuration and retry ${action}.`, account.address);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function requireAccount(account, accountAddress, message) {
|
|
265
|
+
return account ?? fail(message, accountAddress);
|
|
266
|
+
}
|
|
267
|
+
async function hashAccountState(account) {
|
|
268
|
+
const owner = getAddressEncoder().encode(account.owner);
|
|
269
|
+
const canonical = new Uint8Array(owner.length + account.data.length);
|
|
270
|
+
canonical.set(owner);
|
|
271
|
+
canonical.set(account.data, owner.length);
|
|
272
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', canonical));
|
|
273
|
+
return [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
274
|
+
}
|
|
275
|
+
async function precondition(account, describes) {
|
|
276
|
+
return Object.freeze({
|
|
277
|
+
address: account.address,
|
|
278
|
+
dataHash: await hashAccountState(account),
|
|
279
|
+
describes,
|
|
280
|
+
kind: 'account-state',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function absentPrecondition(accountAddress, describes) {
|
|
284
|
+
return Object.freeze({
|
|
285
|
+
address: accountAddress,
|
|
286
|
+
describes,
|
|
287
|
+
kind: 'account-absent',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function findDefinition(configuration, id, game) {
|
|
291
|
+
const definition = configuration.claimStakes.definitions.find((candidate) => candidate.id === id);
|
|
292
|
+
return (definition ??
|
|
293
|
+
fail(`Claim Stake definition ${String(id)} is not a member of the configured Game catalog. Choose an exact current definition id, refresh the Game definitions, and retry.`, game));
|
|
294
|
+
}
|
|
295
|
+
function findHub(configuration, id, game) {
|
|
296
|
+
const hub = configuration.buildings.definitions.find((candidate) => candidate.id === id);
|
|
297
|
+
return (hub ??
|
|
298
|
+
fail(`Hub building ${String(id)} is not a member of the configured Game building catalog. Choose an exact current bundled hub id, refresh the Game definitions, and retry.`, game));
|
|
299
|
+
}
|
|
300
|
+
function claimedCount(details, definitionId) {
|
|
301
|
+
return (details.claimedPlots.find(({ key }) => key === definitionId)?.structures
|
|
302
|
+
.length ?? 0);
|
|
303
|
+
}
|
|
304
|
+
/** Plans placement of one Claim Stake with its exact bundled hub.
|
|
305
|
+
*
|
|
306
|
+
* The planner validates the configured Game/Profile/Character/System/Body
|
|
307
|
+
* graph, current catalog membership, known plot/cargo/crew availability, and
|
|
308
|
+
* absence of the caller-created Claim Stake account. It returns one inert Plan;
|
|
309
|
+
* permissions, placement races, rent arithmetic, and program capacity remain
|
|
310
|
+
* chain-authoritative.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```ts
|
|
314
|
+
* declare const ctx: SageContext;
|
|
315
|
+
* declare const body: CelestialBodySnapshot;
|
|
316
|
+
* declare const player: StarbasePlayerSnapshot;
|
|
317
|
+
* declare const options: PlanPlaceClaimStakeOptions;
|
|
318
|
+
* const plan = await planPlaceClaimStake(ctx, body, player, options);
|
|
319
|
+
* console.log(plan.describe());
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
export async function planPlaceClaimStake(ctx, body, player, input) {
|
|
323
|
+
const options = validateOptions(input);
|
|
324
|
+
const game = requireGameAddress(ctx);
|
|
325
|
+
if (body.kind !== 'planet') {
|
|
326
|
+
fail(`Celestial Body ${body.name} (${body.address}) is an Asteroid, but Claim Stakes require Planet plots. Choose a Planet, refresh it, and retry.`, body.address);
|
|
327
|
+
}
|
|
328
|
+
if (body.game !== game) {
|
|
329
|
+
fail(`Celestial Body ${body.address} does not match the configured Game ${game}. Refresh the Body through this context and retry.`, body.address);
|
|
330
|
+
}
|
|
331
|
+
const canonicalBody = await deriveCelestialBodyAddress(game, body.id);
|
|
332
|
+
if (canonicalBody !== body.address) {
|
|
333
|
+
fail(`Celestial Body ${body.address} is not the canonical Body ${String(body.id)} for the configured Game. Refresh the Body and retry.`, body.address);
|
|
334
|
+
}
|
|
335
|
+
if (player.playerProfile.address !== options.authorization.profile) {
|
|
336
|
+
fail(`Starbase Player ${player.address} does not belong to authorization Profile ${options.authorization.profile}. Refresh the Starbase Player for that Profile and retry.`, player.address);
|
|
337
|
+
}
|
|
338
|
+
const character = await findCharacterPda(options.authorization.profile, game);
|
|
339
|
+
if (player.character.address !== character) {
|
|
340
|
+
fail(`Starbase Player ${player.address} does not reference the canonical Character ${character} for authorization Profile ${options.authorization.profile}. Refresh the Starbase Player and retry.`, player.address);
|
|
341
|
+
}
|
|
342
|
+
const canonicalPlayer = await deriveStarbasePlayerAddress(player.system.address, character);
|
|
343
|
+
if (canonicalPlayer !== player.address) {
|
|
344
|
+
fail(`Starbase Player ${player.address} is not canonical for its Character and System. Refresh the Starbase Player and retry.`, player.address);
|
|
345
|
+
}
|
|
346
|
+
if (body.system.address !== player.system.address) {
|
|
347
|
+
fail(`Celestial Body ${body.address} belongs to System ${body.system.address}, but Starbase Player ${player.address} belongs to System ${player.system.address}. Choose both snapshots from the same System and retry.`, body.address);
|
|
348
|
+
}
|
|
349
|
+
const [freshGame, currentAccounts] = await Promise.all([
|
|
350
|
+
readFreshGameClaimStakeConfiguration(ctx),
|
|
351
|
+
readContextRawAccounts(ctx, [
|
|
352
|
+
body.address,
|
|
353
|
+
player.address,
|
|
354
|
+
character,
|
|
355
|
+
body.system.address,
|
|
356
|
+
options.claimStake,
|
|
357
|
+
]),
|
|
358
|
+
]);
|
|
359
|
+
if (freshGame === undefined) {
|
|
360
|
+
fail(`Configured Game ${game} is missing. Refresh the Game catalog and retry placement.`, game);
|
|
361
|
+
}
|
|
362
|
+
const definition = findDefinition(freshGame.data, options.claimStakeDefinitionId, game);
|
|
363
|
+
const hub = findHub(freshGame.data, options.hubBuildingId, game);
|
|
364
|
+
if (hub.hubValue !== definition.hubValue ||
|
|
365
|
+
hub.minimumTier !== definition.tier ||
|
|
366
|
+
hub.constructionCost.length !== 0) {
|
|
367
|
+
fail(`Hub building ${hub.id} (${hub.name}) is not the exact zero-cost ${definition.tier} bundled hub for Claim Stake definition ${definition.id} (${definition.name}). Choose the matching bundled hub from the current Game catalog and retry.`, game);
|
|
368
|
+
}
|
|
369
|
+
const missingTag = definition.requiredTagIds.find((tag) => !body.details.providedTags.includes(tag));
|
|
370
|
+
if (missingTag !== undefined) {
|
|
371
|
+
fail(`Planet ${body.name} (${body.address}) does not provide required Claim Stake tag ${String(missingTag)} for ${definition.name}. Choose an eligible Planet, refresh it, and retry.`, body.address);
|
|
372
|
+
}
|
|
373
|
+
const cargo = player.cargo.items.find(({ id }) => id === definition.cargoId);
|
|
374
|
+
const cargoQuantity = cargo?.quantityRaw ?? 0n;
|
|
375
|
+
const cargoName = cargo?.name ?? `cargo ${String(definition.cargoId)}`;
|
|
376
|
+
if (cargoQuantity < 1n) {
|
|
377
|
+
fail(`Starbase Player ${player.address} has ${String(cargoQuantity)} raw ${cargoName}, but placing ${definition.name} requires one known unit. Add it, refresh the Starbase Player, and retry.`, player.address);
|
|
378
|
+
}
|
|
379
|
+
if (player.availableCrew < hub.neededCrew) {
|
|
380
|
+
fail(`Starbase Player ${player.address} has only ${String(player.availableCrew)} crew available, but bundled hub ${hub.name} requires ${String(hub.neededCrew)}. Free crew, refresh the Starbase Player, and retry.`, player.address);
|
|
381
|
+
}
|
|
382
|
+
const [bodyAccount, playerAccount, characterAccount, systemAccount, newStake,] = currentAccounts;
|
|
383
|
+
if (newStake !== undefined) {
|
|
384
|
+
fail(`Claim Stake signer address ${options.claimStake} already exists. Generate a fresh address-only Claim Stake signer and retry.`, options.claimStake);
|
|
385
|
+
}
|
|
386
|
+
const currentBodyAccount = requireAccount(bodyAccount, body.address, `Celestial Body ${body.address} is missing. Refresh the Body and retry placement.`);
|
|
387
|
+
const currentPlayerAccount = requireAccount(playerAccount, player.address, `Starbase Player ${player.address} is missing. Refresh it and retry placement.`);
|
|
388
|
+
const currentCharacterAccount = requireAccount(characterAccount, character, `Canonical Character ${character} is missing. Refresh the Profile Character and retry placement.`);
|
|
389
|
+
const currentSystemAccount = requireAccount(systemAccount, body.system.address, `Star System ${body.system.address} is missing. Refresh the System and retry placement.`);
|
|
390
|
+
const currentBody = celestialBodyAccountDefinition.decode(currentBodyAccount);
|
|
391
|
+
const currentPlayer = starbasePlayerAccountDefinition.decode(currentPlayerAccount);
|
|
392
|
+
const currentCharacter = characterAccountDefinition.decode(currentCharacterAccount);
|
|
393
|
+
const currentSystem = starSystemAccountDefinition.decode(currentSystemAccount);
|
|
394
|
+
const canonicalSystem = await deriveStarSystemAddress(game, currentSystem.systemId);
|
|
395
|
+
if (canonicalSystem !== body.system.address ||
|
|
396
|
+
currentSystem.game !== game ||
|
|
397
|
+
currentSystem.starbase === undefined) {
|
|
398
|
+
fail(`Star System ${body.system.address} does not match the configured Game, canonical System address, or an active nested Starbase. Refresh the System and retry.`, body.system.address);
|
|
399
|
+
}
|
|
400
|
+
if (currentBody.kind !== 'planet' ||
|
|
401
|
+
currentBody.game !== game ||
|
|
402
|
+
currentBody.system !== body.system.address ||
|
|
403
|
+
currentBody.id !== body.id ||
|
|
404
|
+
currentBody.systemSequenceId !== body.systemSequenceId ||
|
|
405
|
+
currentBody.systemSequenceId !== currentSystem.sequenceId) {
|
|
406
|
+
fail(`Celestial Body ${body.address} changed or does not match its configured Game, System, id, Planet kind, or System sequence. Refresh the Body and System and retry.`, body.address);
|
|
407
|
+
}
|
|
408
|
+
if (currentPlayer.playerProfile !== options.authorization.profile ||
|
|
409
|
+
currentPlayer.character !== character ||
|
|
410
|
+
currentPlayer.system !== body.system.address ||
|
|
411
|
+
currentPlayer.totalCrew - currentPlayer.busyCrew !== player.availableCrew) {
|
|
412
|
+
fail(`Starbase Player ${player.address} changed or does not match the authorization Profile, canonical Character, System, or available crew. Refresh it and retry.`, player.address);
|
|
413
|
+
}
|
|
414
|
+
if (currentCharacter.playerProfile !== options.authorization.profile ||
|
|
415
|
+
currentCharacter.game !== game) {
|
|
416
|
+
fail(`Character ${character} does not match authorization Profile ${options.authorization.profile} and configured Game ${game}. Refresh the Character and retry.`, character);
|
|
417
|
+
}
|
|
418
|
+
const currentCargo = currentPlayer.cargo.amounts.find(({ cargoId }) => cargoId === definition.cargoId)?.amount ?? 0n;
|
|
419
|
+
if (currentCargo !== cargoQuantity || currentCargo < 1n) {
|
|
420
|
+
fail(`Starbase Player ${player.address} Claim Stake cargo changed from the loaded snapshot or is no longer sufficient. Refresh its cargo and retry.`, player.address);
|
|
421
|
+
}
|
|
422
|
+
const currentDetails = currentBody.details;
|
|
423
|
+
const currentMissingTag = definition.requiredTagIds.find((tag) => !currentDetails.providedTags.includes(tag));
|
|
424
|
+
if (currentMissingTag !== undefined) {
|
|
425
|
+
fail(`Planet ${body.name} (${body.address}) no longer provides required Claim Stake tag ${String(currentMissingTag)} for ${definition.name}. Refresh it and retry.`, body.address);
|
|
426
|
+
}
|
|
427
|
+
const level = LEVEL_INDEX[currentSystem.starbase.level];
|
|
428
|
+
const tier = TIER_INDEX[definition.tier];
|
|
429
|
+
const plotLimit = currentDetails.claimStakePlots[level][tier];
|
|
430
|
+
const usedPlots = claimedCount(currentDetails, definition.id);
|
|
431
|
+
if (usedPlots >= plotLimit) {
|
|
432
|
+
fail(`Planet ${body.name} (${body.address}) has no available ${definition.name} plot at Starbase ${currentSystem.starbase.level}; ${String(usedPlots)} of ${String(plotLimit)} known plots are occupied. Choose another eligible Planet or tier, refresh it, and retry.`, body.address);
|
|
433
|
+
}
|
|
434
|
+
const profileFaction = await deriveProfileFactionAddress(options.authorization.profile);
|
|
435
|
+
const currencyCache = await deriveCurrencyCacheAddress(game);
|
|
436
|
+
const description = `Place ${definition.name} on Planet ${body.name} (${body.address}) with bundled hub ${hub.name}, ${String(hub.neededCrew)} crew, and ${String(options.initialRentAmount)} raw initial rent.`;
|
|
437
|
+
return createPlan({
|
|
438
|
+
kind: 'claim-stakes.place',
|
|
439
|
+
preconditions: [
|
|
440
|
+
...(await Promise.all([
|
|
441
|
+
precondition(freshGame.account, `The configured Game retains Claim Stake definition ${String(definition.id)} and bundled hub ${String(hub.id)} eligibility.`),
|
|
442
|
+
precondition(currentBodyAccount, `Planet ${body.name} retains an available ${definition.name} plot and its configured Game/System relationship.`),
|
|
443
|
+
precondition(currentPlayerAccount, `Starbase Player ${player.address} retains its Profile, Character, System, available crew, and ${cargoName} balance.`),
|
|
444
|
+
precondition(currentCharacterAccount, `The canonical Character remains assigned to Profile ${options.authorization.profile} in the configured Game.`),
|
|
445
|
+
precondition(currentSystemAccount, `Star System ${currentSystem.name} retains its configured Game, sequence, and active Starbase level.`),
|
|
446
|
+
])),
|
|
447
|
+
absentPrecondition(options.claimStake, 'The caller-supplied Claim Stake signer address remains unallocated.'),
|
|
448
|
+
],
|
|
449
|
+
steps: [
|
|
450
|
+
{
|
|
451
|
+
describes: description,
|
|
452
|
+
instruction: adaptPlaceClaimStakeInstruction({
|
|
453
|
+
authorization: options.authorization,
|
|
454
|
+
celestialBody: body.address,
|
|
455
|
+
character,
|
|
456
|
+
claimStakeDefinitionId: definition.id,
|
|
457
|
+
claimStakeInstance: options.claimStake,
|
|
458
|
+
currencyCache,
|
|
459
|
+
game,
|
|
460
|
+
hubBuildingId: hub.id,
|
|
461
|
+
initialRentAmount: options.initialRentAmount,
|
|
462
|
+
profileFaction,
|
|
463
|
+
systemAndStarbasePlayerStarbasePlayer: player.address,
|
|
464
|
+
systemAndStarbasePlayerSystem: body.system.address,
|
|
465
|
+
}),
|
|
466
|
+
signers: [options.authorization.authority, options.claimStake],
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
summary: description,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
function quantities(buildings) {
|
|
473
|
+
return new Map(buildings.map(({ buildingId, quantity }) => [buildingId, quantity]));
|
|
474
|
+
}
|
|
475
|
+
function sameQuantities(left, right) {
|
|
476
|
+
const leftById = quantities(left);
|
|
477
|
+
const rightById = quantities(right);
|
|
478
|
+
return (leftById.size === left.length &&
|
|
479
|
+
rightById.size === right.length &&
|
|
480
|
+
leftById.size === rightById.size &&
|
|
481
|
+
[...leftById].every(([buildingId, quantity]) => rightById.get(buildingId) === quantity));
|
|
482
|
+
}
|
|
483
|
+
function sameStakeState(left, right) {
|
|
484
|
+
if (left.kind === 'deactivated')
|
|
485
|
+
return false;
|
|
486
|
+
switch (left.kind) {
|
|
487
|
+
case 'active':
|
|
488
|
+
return right.kind === 'active';
|
|
489
|
+
case 'design':
|
|
490
|
+
return (right.kind === 'design' &&
|
|
491
|
+
left.sequenceId === right.sequenceId &&
|
|
492
|
+
sameQuantities(left.buildings, right.buildings));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const LEGACY_CONTROLLER_ID = Object.freeze({
|
|
496
|
+
unaligned: 0,
|
|
497
|
+
mud: 1,
|
|
498
|
+
oni: 2,
|
|
499
|
+
ustur: 3,
|
|
500
|
+
});
|
|
501
|
+
function buildingDescription(change, name) {
|
|
502
|
+
return `${change.kind} ${String(change.quantity)} ${name}`;
|
|
503
|
+
}
|
|
504
|
+
/** Plans one exact set of additions/removals for an owned active/design Claim Stake.
|
|
505
|
+
*
|
|
506
|
+
* The planner verifies the fresh Claim Stake, definition sequences, canonical
|
|
507
|
+
* Profile/Character/Body/System/Starbase Player graph, optional
|
|
508
|
+
* FactionOwnership sidecar, building quantities, slots, crew, and known
|
|
509
|
+
* construction cargo. Rent synchronization and the final concurrent sequence
|
|
510
|
+
* race remain chain-authoritative.
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* ```ts
|
|
514
|
+
* declare const ctx: SageContext;
|
|
515
|
+
* declare const stake: ClaimStakeSnapshot;
|
|
516
|
+
* declare const options: PlanClaimStakeBuildingChangesOptions;
|
|
517
|
+
* const plan = await planClaimStakeBuildingChanges(ctx, stake, options);
|
|
518
|
+
* console.log(plan.describe());
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
export async function planClaimStakeBuildingChanges(ctx, stake, input) {
|
|
522
|
+
const options = validateBuildingChangeOptions(input);
|
|
523
|
+
const game = requireGameAddress(ctx);
|
|
524
|
+
if (stake.state.kind === 'deactivated') {
|
|
525
|
+
fail(`Claim Stake ${stake.address} is deactivated; building changes require an active or design Claim Stake. Refresh it or choose another owned Claim Stake before retrying.`, stake.address);
|
|
526
|
+
}
|
|
527
|
+
if (stake.game !== game) {
|
|
528
|
+
fail(`Claim Stake ${stake.address} does not match the configured Game ${game}. Refresh it through this context and retry.`, stake.address);
|
|
529
|
+
}
|
|
530
|
+
const character = await findCharacterPda(options.authorization.profile, game);
|
|
531
|
+
if (stake.ownerCharacter.address !== character) {
|
|
532
|
+
fail(`Claim Stake ${stake.address} does not belong to authorization Profile ${options.authorization.profile}'s canonical Character ${character}. Choose an owned Claim Stake, refresh it, and retry.`, stake.address);
|
|
533
|
+
}
|
|
534
|
+
const [freshGame, firstAccounts] = await Promise.all([
|
|
535
|
+
readFreshGameClaimStakeConfiguration(ctx),
|
|
536
|
+
readContextRawAccounts(ctx, [stake.address, stake.body.address, character]),
|
|
537
|
+
]);
|
|
538
|
+
if (freshGame === undefined) {
|
|
539
|
+
fail(`Configured Game ${game} is missing. Refresh the Game catalog and retry building changes.`, game);
|
|
540
|
+
}
|
|
541
|
+
const stakeAccount = requireAccount(firstAccounts[0], stake.address, `Claim Stake ${stake.address} is missing. Refresh it and retry building changes.`);
|
|
542
|
+
const bodyAccount = requireAccount(firstAccounts[1], stake.body.address, `Celestial Body ${stake.body.address} is missing. Refresh the Claim Stake Body and retry building changes.`);
|
|
543
|
+
const characterAccount = requireAccount(firstAccounts[2], character, `Canonical Character ${character} is missing. Refresh the Profile Character and retry building changes.`);
|
|
544
|
+
const currentStake = claimStakeAccountDefinition.decode(stakeAccount);
|
|
545
|
+
const currentBody = celestialBodyAccountDefinition.decode(bodyAccount);
|
|
546
|
+
const currentCharacter = characterAccountDefinition.decode(characterAccount);
|
|
547
|
+
if (currentStake.game !== game ||
|
|
548
|
+
currentStake.body !== stake.body.address ||
|
|
549
|
+
currentStake.ownerCharacter !== character ||
|
|
550
|
+
currentStake.claimStakeDefinitionId !== stake.claimStakeDefinitionId ||
|
|
551
|
+
currentStake.claimStakeDefinitionSeqId !==
|
|
552
|
+
stake.claimStakeDefinitionSeqId ||
|
|
553
|
+
currentStake.buildingDefinitionSeqId !== stake.buildingDefinitionSeqId ||
|
|
554
|
+
currentStake.systemSeqId !== stake.systemSeqId ||
|
|
555
|
+
!sameQuantities(currentStake.buildings, stake.buildings) ||
|
|
556
|
+
currentStake.neededCrew !== stake.neededCrew) {
|
|
557
|
+
fail(`Claim Stake ${stake.address} changed or does not match its loaded ownership, Body, definitions, buildings, crew, or System generation. Refresh it and retry building changes.`, stake.address);
|
|
558
|
+
}
|
|
559
|
+
if (currentCharacter.playerProfile !== options.authorization.profile ||
|
|
560
|
+
currentCharacter.game !== game) {
|
|
561
|
+
fail(`Character ${character} does not match authorization Profile ${options.authorization.profile} and configured Game ${game}. Refresh the Character and retry building changes.`, character);
|
|
562
|
+
}
|
|
563
|
+
if (currentBody.kind !== 'planet' ||
|
|
564
|
+
currentBody.game !== game ||
|
|
565
|
+
currentBody.systemSequenceId !== stake.systemSeqId ||
|
|
566
|
+
(await deriveCelestialBodyAddress(game, currentBody.id)) !==
|
|
567
|
+
stake.body.address) {
|
|
568
|
+
fail(`Celestial Body ${stake.body.address} does not match the Claim Stake's configured Game, canonical Planet identity, or System generation. Refresh the Body and Claim Stake before retrying.`, stake.body.address);
|
|
569
|
+
}
|
|
570
|
+
const system = currentBody.system;
|
|
571
|
+
const [player, ownership, profileFaction] = await Promise.all([
|
|
572
|
+
deriveStarbasePlayerAddress(system, character),
|
|
573
|
+
deriveFactionOwnershipAddress(game, 'starbase', system),
|
|
574
|
+
deriveProfileFactionAddress(options.authorization.profile),
|
|
575
|
+
]);
|
|
576
|
+
const [systemAccountCandidate, playerAccountCandidate, ownershipAccount, profileFactionAccountCandidate,] = await readContextRawAccounts(ctx, [
|
|
577
|
+
system,
|
|
578
|
+
player,
|
|
579
|
+
ownership,
|
|
580
|
+
profileFaction,
|
|
581
|
+
]);
|
|
582
|
+
const systemAccount = requireAccount(systemAccountCandidate, system, `Star System ${system} is missing. Refresh the Claim Stake's System and retry building changes.`);
|
|
583
|
+
const playerAccount = requireAccount(playerAccountCandidate, player, `Starbase Player ${player} is missing for authorization Profile ${options.authorization.profile}. Register or refresh it in the Claim Stake's System before retrying.`);
|
|
584
|
+
const profileFactionAccount = requireAccount(profileFactionAccountCandidate, profileFaction, `ProfileFaction ${profileFaction} is missing for authorization Profile ${options.authorization.profile}. Choose or repair the Profile faction, refresh it, and retry building changes.`);
|
|
585
|
+
const currentSystem = starSystemAccountDefinition.decode(systemAccount);
|
|
586
|
+
const currentPlayer = starbasePlayerAccountDefinition.decode(playerAccount);
|
|
587
|
+
const currentProfileFaction = profileFactionIdentityAccountDefinition.decode(profileFactionAccount);
|
|
588
|
+
if (currentSystem.game !== game ||
|
|
589
|
+
currentSystem.sequenceId !== stake.systemSeqId ||
|
|
590
|
+
currentSystem.starbase === undefined ||
|
|
591
|
+
(await deriveStarSystemAddress(game, currentSystem.systemId)) !== system) {
|
|
592
|
+
fail(`Star System ${system} does not match the configured Game, canonical identity, Claim Stake generation, or an active nested Starbase. Refresh the System and retry building changes.`, system);
|
|
593
|
+
}
|
|
594
|
+
if (currentPlayer.playerProfile !== options.authorization.profile ||
|
|
595
|
+
currentPlayer.character !== character ||
|
|
596
|
+
currentPlayer.system !== system) {
|
|
597
|
+
fail(`Starbase Player ${player} does not match the authorization Profile, canonical Character, and Claim Stake System. Refresh it and retry building changes.`, player);
|
|
598
|
+
}
|
|
599
|
+
if (currentProfileFaction.profile !== options.authorization.profile) {
|
|
600
|
+
fail(`ProfileFaction ${profileFaction} does not match authorization Profile ${options.authorization.profile}. Refresh or repair the Profile faction before retrying building changes.`, profileFaction);
|
|
601
|
+
}
|
|
602
|
+
let controllerFactionId = LEGACY_CONTROLLER_ID[currentSystem.starbase.owner];
|
|
603
|
+
let ownershipPrecondition;
|
|
604
|
+
if (ownershipAccount === undefined) {
|
|
605
|
+
ownershipPrecondition = absentPrecondition(ownership, `The canonical Starbase FactionOwnership ${ownership} remains unallocated so the chain applies its legacy controller gate.`);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
const currentOwnership = factionOwnershipAccountDefinition.decode(ownershipAccount);
|
|
609
|
+
if (currentOwnership.game !== game ||
|
|
610
|
+
currentOwnership.asset.kind !== 'starbase' ||
|
|
611
|
+
currentOwnership.asset.system !== system ||
|
|
612
|
+
currentOwnership.capturedSystemSequenceId !== currentSystem.sequenceId) {
|
|
613
|
+
fail(`FactionOwnership ${ownership} does not match the configured Game, Claim Stake System, or current System generation. Refresh the faction relationship and retry building changes.`, ownership);
|
|
614
|
+
}
|
|
615
|
+
controllerFactionId =
|
|
616
|
+
currentOwnership.controllingFactionId ?? controllerFactionId;
|
|
617
|
+
ownershipPrecondition = await precondition(ownershipAccount, `The Starbase FactionOwnership remains assigned to System ${system} at its current generation.`);
|
|
618
|
+
}
|
|
619
|
+
if (currentProfileFaction.factionId !== controllerFactionId) {
|
|
620
|
+
fail(`ProfileFaction ${profileFaction} faction ${String(currentProfileFaction.factionId)} does not match the current Starbase controller faction ${String(controllerFactionId)}. Use a Profile aligned with the current controller, refresh the faction relationship, and retry building changes.`, profileFaction);
|
|
621
|
+
}
|
|
622
|
+
if (freshGame.data.claimStakes.sequenceId !== stake.claimStakeDefinitionSeqId ||
|
|
623
|
+
freshGame.data.buildings.sequenceId !== stake.buildingDefinitionSeqId) {
|
|
624
|
+
fail(`Claim Stake ${stake.address} definition sequence is stale against the current Game catalog. Refresh the Claim Stake and definitions to rule out stale local data. If the on-chain generation still differs, use a Claim Stake matching the current catalog before retrying building changes.`, stake.address);
|
|
625
|
+
}
|
|
626
|
+
const claimDefinition = freshGame.data.claimStakes.definitions.find(({ id }) => id === stake.claimStakeDefinitionId);
|
|
627
|
+
if (claimDefinition === undefined) {
|
|
628
|
+
fail(`Claim Stake definition ${String(stake.claimStakeDefinitionId)} is absent from the current Game catalog. Refresh the Claim Stake and definitions before retrying.`, game);
|
|
629
|
+
}
|
|
630
|
+
const sourceBuildings = stake.state.kind === 'design' ? stake.state.buildings : stake.buildings;
|
|
631
|
+
if (currentStake.state.kind !== stake.state.kind ||
|
|
632
|
+
(stake.state.kind === 'design' &&
|
|
633
|
+
(currentStake.state.kind !== 'design' ||
|
|
634
|
+
currentStake.state.sequenceId !== stake.state.sequenceId ||
|
|
635
|
+
!sameQuantities(currentStake.state.buildings, stake.state.buildings)))) {
|
|
636
|
+
fail(`Claim Stake ${stake.address} lifecycle state or exact design sequence changed. Refresh it and retry building changes.`, stake.address);
|
|
637
|
+
}
|
|
638
|
+
const sequenceId = stake.state.kind === 'design' ? stake.state.sequenceId : 0;
|
|
639
|
+
const result = quantities(sourceBuildings);
|
|
640
|
+
const definitions = new Map(freshGame.data.buildings.definitions.map((definition) => [
|
|
641
|
+
definition.id,
|
|
642
|
+
definition,
|
|
643
|
+
]));
|
|
644
|
+
const descriptions = [];
|
|
645
|
+
for (const change of options.buildingChanges) {
|
|
646
|
+
const definition = definitions.get(change.buildingId);
|
|
647
|
+
if (definition === undefined) {
|
|
648
|
+
fail(`Building ${String(change.buildingId)} has no definition in the current Game catalog. Refresh the definitions and choose a valid building before retrying.`, game);
|
|
649
|
+
}
|
|
650
|
+
const current = result.get(change.buildingId) ?? 0;
|
|
651
|
+
const next = change.kind === 'add'
|
|
652
|
+
? current + change.quantity
|
|
653
|
+
: current - change.quantity;
|
|
654
|
+
if (next < 0) {
|
|
655
|
+
fail(`Cannot remove ${String(change.quantity)} ${definition.name} from Claim Stake ${stake.address}; the resulting quantity would underflow from ${String(current)}. Reduce the removal and retry.`, stake.address);
|
|
656
|
+
}
|
|
657
|
+
if (next > U32_MAX) {
|
|
658
|
+
fail(`Building ${definition.name} quantity would overflow unsigned u32 after this addition. Reduce the quantity and retry.`, stake.address);
|
|
659
|
+
}
|
|
660
|
+
if (next === 0)
|
|
661
|
+
result.delete(change.buildingId);
|
|
662
|
+
else
|
|
663
|
+
result.set(change.buildingId, next);
|
|
664
|
+
descriptions.push(buildingDescription(change, definition.name));
|
|
665
|
+
}
|
|
666
|
+
let usedSlots = 0;
|
|
667
|
+
let crewSlots = 0;
|
|
668
|
+
let neededCrew = 0;
|
|
669
|
+
let resourceStorageRaw = 0n;
|
|
670
|
+
const providedTags = new Set(claimDefinition.providedTagIds);
|
|
671
|
+
for (const [buildingId, quantity] of result) {
|
|
672
|
+
const definition = definitions.get(buildingId);
|
|
673
|
+
if (definition === undefined) {
|
|
674
|
+
fail(`Existing building ${String(buildingId)} has no definition in the current Game catalog. Refresh the Claim Stake and definitions before retrying.`, game);
|
|
675
|
+
}
|
|
676
|
+
if (TIER_INDEX[definition.minimumTier] > TIER_INDEX[claimDefinition.tier]) {
|
|
677
|
+
fail(`Building ${definition.name} requires ${definition.minimumTier}, above Claim Stake ${claimDefinition.name}'s ${claimDefinition.tier} tier. Remove that building from the design before retrying.`, stake.address);
|
|
678
|
+
}
|
|
679
|
+
if (definition.hubValue !== 0 &&
|
|
680
|
+
definition.hubValue !== claimDefinition.hubValue) {
|
|
681
|
+
fail(`Hub building ${definition.name} does not match Claim Stake ${claimDefinition.name}'s hub value. Choose the matching hub before retrying.`, stake.address);
|
|
682
|
+
}
|
|
683
|
+
usedSlots += definition.slots * quantity;
|
|
684
|
+
crewSlots += definition.crewSlots * quantity;
|
|
685
|
+
neededCrew += definition.neededCrew * quantity;
|
|
686
|
+
resourceStorageRaw += definition.resourceStorageRaw * BigInt(quantity);
|
|
687
|
+
if (neededCrew > U16_MAX) {
|
|
688
|
+
fail(`The resulting Claim Stake design has ${String(neededCrew)} needed crew, exceeding the ${String(U16_MAX)} maximum representable by its unsigned u16 needed-crew field. Remove crew-requiring buildings and retry.`, stake.address);
|
|
689
|
+
}
|
|
690
|
+
if (resourceStorageRaw > U64_MAX) {
|
|
691
|
+
fail(`The resulting Claim Stake design provides ${String(resourceStorageRaw)} raw resource storage, exceeding the ${String(U64_MAX)} maximum representable by its unsigned u64 resource-storage field. Reduce storage-building quantities and retry.`, stake.address);
|
|
692
|
+
}
|
|
693
|
+
definition.providedTagIds.forEach((tag) => providedTags.add(tag));
|
|
694
|
+
}
|
|
695
|
+
if (usedSlots > claimDefinition.slots) {
|
|
696
|
+
fail(`Building changes require ${String(usedSlots)} slots, exceeding Claim Stake ${claimDefinition.name}'s ${String(claimDefinition.slots)} slot capacity. Remove buildings and retry.`, stake.address);
|
|
697
|
+
}
|
|
698
|
+
if (neededCrew > crewSlots) {
|
|
699
|
+
fail(`Building changes require ${String(neededCrew)} crew, exceeding the resulting ${String(crewSlots)} crew capacity. Add crew-slot capacity or remove crew-requiring buildings before retrying.`, stake.address);
|
|
700
|
+
}
|
|
701
|
+
for (const [buildingId] of result) {
|
|
702
|
+
const definition = definitions.get(buildingId);
|
|
703
|
+
const missingTag = definition.requiredTagIds.find((tag) => !providedTags.has(tag));
|
|
704
|
+
if (missingTag !== undefined) {
|
|
705
|
+
fail(`Building ${definition.name} requires tag ${String(missingTag)}, which the resulting Claim Stake design does not provide. Add a compatible building or remove it before retrying.`, stake.address);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const availableCrew = currentPlayer.totalCrew - currentPlayer.busyCrew;
|
|
709
|
+
const addedCrew = options.buildingChanges.reduce((total, change) => {
|
|
710
|
+
const definition = definitions.get(change.buildingId);
|
|
711
|
+
return (total +
|
|
712
|
+
(change.kind === 'add' ? definition.neededCrew * change.quantity : 0));
|
|
713
|
+
}, 0);
|
|
714
|
+
if (addedCrew > availableCrew) {
|
|
715
|
+
fail(`Building additions require ${String(addedCrew)} crew, but Starbase Player ${player} has only ${String(availableCrew)} known crew capacity available. Free crew, refresh the Starbase Player, and retry.`, player);
|
|
716
|
+
}
|
|
717
|
+
const cargo = new Map(currentStake.production.cargoPod.amounts.map(({ cargoId, amount }) => [
|
|
718
|
+
cargoId,
|
|
719
|
+
amount,
|
|
720
|
+
]));
|
|
721
|
+
const constructionCosts = new Map();
|
|
722
|
+
for (const change of options.buildingChanges) {
|
|
723
|
+
if (change.kind !== 'add')
|
|
724
|
+
continue;
|
|
725
|
+
const definition = definitions.get(change.buildingId);
|
|
726
|
+
for (const cost of definition.constructionCost) {
|
|
727
|
+
const required = cost.quantityRaw * BigInt(change.quantity);
|
|
728
|
+
constructionCosts.set(cost.cargoId, (constructionCosts.get(cost.cargoId) ?? 0n) + required);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (const [cargoId, required] of constructionCosts) {
|
|
732
|
+
const available = cargo.get(cargoId) ?? 0n;
|
|
733
|
+
if (required > available) {
|
|
734
|
+
const snapshotItem = stake.resources.inventory.items.find(({ id }) => id === cargoId);
|
|
735
|
+
fail(`Building construction requires ${String(required)} raw ${snapshotItem?.name ?? `cargo ${String(cargoId)}`}, but Claim Stake ${stake.address} has only ${String(available)} known. Add construction cargo, refresh the Claim Stake, and retry.`, stake.address);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (currentStake.production.resourceCapacityRaw !==
|
|
739
|
+
stake.resources.capacity.totalRaw ||
|
|
740
|
+
currentStake.production.cargoPod.storageCost !==
|
|
741
|
+
stake.resources.inventory.usedStorageUnitsRaw) {
|
|
742
|
+
fail(`Claim Stake ${stake.address} cargo capacity changed from the loaded snapshot. Refresh its resources and retry building changes.`, stake.address);
|
|
743
|
+
}
|
|
744
|
+
if (currentStake.production.cargoPod.storageCost > resourceStorageRaw) {
|
|
745
|
+
fail(`The resulting Claim Stake design provides ${String(resourceStorageRaw)} raw storage capacity, below the fresh known cargo usage of ${String(currentStake.production.cargoPod.storageCost)}. Keep enough storage buildings, remove cargo, refresh the Claim Stake, and retry building changes.`, stake.address);
|
|
746
|
+
}
|
|
747
|
+
const currencyCache = await deriveCurrencyCacheAddress(game);
|
|
748
|
+
const description = `Change buildings on Claim Stake ${stake.address}: ${descriptions.join('; ')}.`;
|
|
749
|
+
return createPlan({
|
|
750
|
+
kind: 'claim-stakes.building-changes',
|
|
751
|
+
preconditions: [
|
|
752
|
+
...(await Promise.all([
|
|
753
|
+
precondition(freshGame.account, 'The configured Game retains the exact Claim Stake and building definitions.'),
|
|
754
|
+
precondition(stakeAccount, `Claim Stake ${stake.address} retains its ownership, design sequence, buildings, cargo, crew, and Body relationship.`),
|
|
755
|
+
precondition(bodyAccount, `The Claim Stake Planet remains assigned to System ${system} at the current generation.`),
|
|
756
|
+
precondition(characterAccount, `The canonical Character remains assigned to Profile ${options.authorization.profile} in the configured Game.`),
|
|
757
|
+
precondition(systemAccount, `Star System ${currentSystem.name} retains its configured Game, sequence, and nested Starbase.`),
|
|
758
|
+
precondition(playerAccount, `Starbase Player ${player} retains its Profile, Character, System, and available crew.`),
|
|
759
|
+
precondition(profileFactionAccount, `ProfileFaction ${profileFaction} remains assigned to Profile ${options.authorization.profile} and aligned with the current Starbase controller.`),
|
|
760
|
+
])),
|
|
761
|
+
ownershipPrecondition,
|
|
762
|
+
],
|
|
763
|
+
steps: [
|
|
764
|
+
{
|
|
765
|
+
describes: description,
|
|
766
|
+
instruction: adaptClaimStakeBuildingChangesInstruction({
|
|
767
|
+
authorization: options.authorization,
|
|
768
|
+
buildingChanges: options.buildingChanges.map(({ buildingId, kind, quantity }) => [buildingId, kind === 'add' ? quantity : -quantity]),
|
|
769
|
+
celestialBody: stake.body.address,
|
|
770
|
+
claimStakeInstance: stake.address,
|
|
771
|
+
currencyCache,
|
|
772
|
+
factionOwnership: ownership,
|
|
773
|
+
game,
|
|
774
|
+
profileFaction,
|
|
775
|
+
sequenceId,
|
|
776
|
+
starbasePlayer: player,
|
|
777
|
+
system,
|
|
778
|
+
}),
|
|
779
|
+
signers: [options.authorization.authority],
|
|
780
|
+
},
|
|
781
|
+
],
|
|
782
|
+
summary: description,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
/** Plans finalization of one owned Claim Stake building design.
|
|
786
|
+
*
|
|
787
|
+
* The planner requires a fresh design state, a coherent configured
|
|
788
|
+
* Game/Character/Planet/System/Starbase Player graph, the canonical
|
|
789
|
+
* CurrencyCache, and a known positive rent balance. Rent timing and the final
|
|
790
|
+
* crew/building validation remain chain-authoritative.
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* ```ts
|
|
794
|
+
* declare const ctx: SageContext;
|
|
795
|
+
* declare const stake: ClaimStakeSnapshot;
|
|
796
|
+
* declare const authorization: PlanAuthorization;
|
|
797
|
+
* const plan = await planFinalizeClaimStakeBuildingChanges(ctx, stake, {
|
|
798
|
+
* authorization,
|
|
799
|
+
* });
|
|
800
|
+
* // The program re-synchronizes rent at execution.
|
|
801
|
+
* console.log(plan.describe());
|
|
802
|
+
* ```
|
|
803
|
+
*/
|
|
804
|
+
export async function planFinalizeClaimStakeBuildingChanges(ctx, stake, input) {
|
|
805
|
+
const options = validateFinalizeBuildingChangeOptions(input);
|
|
806
|
+
const game = requireGameAddress(ctx);
|
|
807
|
+
if (stake.state.kind !== 'design') {
|
|
808
|
+
fail(`Claim Stake ${stake.address} is not in design state. Refresh it and finalize only a pending building design.`, stake.address);
|
|
809
|
+
}
|
|
810
|
+
if (stake.game !== game) {
|
|
811
|
+
fail(`Claim Stake ${stake.address} does not match the configured Game ${game}. Refresh it through this context and retry.`, stake.address);
|
|
812
|
+
}
|
|
813
|
+
const character = await findCharacterPda(options.authorization.profile, game);
|
|
814
|
+
if (stake.ownerCharacter.address !== character) {
|
|
815
|
+
fail(`Claim Stake ${stake.address} does not belong to authorization Profile ${options.authorization.profile}'s canonical Character ${character}. Choose an owned Claim Stake, refresh it, and retry.`, stake.address);
|
|
816
|
+
}
|
|
817
|
+
if (stake.rentBalanceRaw <= 0n) {
|
|
818
|
+
fail(`Claim Stake ${stake.address} has a known delinquent rent balance of ${String(stake.rentBalanceRaw)} raw units. Top up rent, refresh the Claim Stake, and retry finalization.`, stake.address);
|
|
819
|
+
}
|
|
820
|
+
const [freshGame, firstAccounts] = await Promise.all([
|
|
821
|
+
readFreshGameClaimStakeConfiguration(ctx),
|
|
822
|
+
readContextRawAccounts(ctx, [stake.address, stake.body.address, character]),
|
|
823
|
+
]);
|
|
824
|
+
if (freshGame === undefined) {
|
|
825
|
+
fail(`Configured Game ${game} is missing. Refresh the Game catalog and retry finalization.`, game);
|
|
826
|
+
}
|
|
827
|
+
if (freshGame.data.claimStakes.sequenceId !== stake.claimStakeDefinitionSeqId ||
|
|
828
|
+
freshGame.data.buildings.sequenceId !== stake.buildingDefinitionSeqId) {
|
|
829
|
+
fail(`Claim Stake ${stake.address} definition sequence is stale against the current Game catalog. Refresh the Claim Stake and definitions to rule out stale local data. If the on-chain generation still differs, use a Claim Stake matching the current catalog before retrying finalization.`, stake.address);
|
|
830
|
+
}
|
|
831
|
+
const stakeAccount = requireAccount(firstAccounts[0], stake.address, `Claim Stake ${stake.address} is missing. Refresh it and retry finalization.`);
|
|
832
|
+
const bodyAccount = requireAccount(firstAccounts[1], stake.body.address, `Celestial Body ${stake.body.address} is missing. Refresh the Claim Stake Body and retry finalization.`);
|
|
833
|
+
const characterAccount = requireAccount(firstAccounts[2], character, `Canonical Character ${character} is missing. Refresh the Profile Character and retry finalization.`);
|
|
834
|
+
const currentStake = claimStakeAccountDefinition.decode(stakeAccount);
|
|
835
|
+
const currentBody = celestialBodyAccountDefinition.decode(bodyAccount);
|
|
836
|
+
const currentCharacter = characterAccountDefinition.decode(characterAccount);
|
|
837
|
+
if (currentStake.game !== game ||
|
|
838
|
+
currentStake.body !== stake.body.address ||
|
|
839
|
+
currentStake.ownerCharacter !== character ||
|
|
840
|
+
currentStake.claimStakeDefinitionId !== stake.claimStakeDefinitionId ||
|
|
841
|
+
currentStake.claimStakeDefinitionSeqId !==
|
|
842
|
+
stake.claimStakeDefinitionSeqId ||
|
|
843
|
+
currentStake.buildingDefinitionSeqId !== stake.buildingDefinitionSeqId ||
|
|
844
|
+
currentStake.systemSeqId !== stake.systemSeqId ||
|
|
845
|
+
currentStake.rentBalanceRaw !== stake.rentBalanceRaw ||
|
|
846
|
+
!sameQuantities(currentStake.buildings, stake.buildings) ||
|
|
847
|
+
currentStake.state.kind !== 'design' ||
|
|
848
|
+
currentStake.state.sequenceId !== stake.state.sequenceId ||
|
|
849
|
+
!sameQuantities(currentStake.state.buildings, stake.state.buildings)) {
|
|
850
|
+
fail(`Claim Stake ${stake.address} changed or does not match its loaded ownership, Body, definitions, buildings, design sequence, rent, or System generation. Refresh it and retry finalization.`, stake.address);
|
|
851
|
+
}
|
|
852
|
+
if (currentCharacter.playerProfile !== options.authorization.profile ||
|
|
853
|
+
currentCharacter.game !== game) {
|
|
854
|
+
fail(`Character ${character} does not match authorization Profile ${options.authorization.profile} and configured Game ${game}. Refresh the Character and retry finalization.`, character);
|
|
855
|
+
}
|
|
856
|
+
if (currentBody.kind !== 'planet' ||
|
|
857
|
+
currentBody.game !== game ||
|
|
858
|
+
currentBody.systemSequenceId !== stake.systemSeqId ||
|
|
859
|
+
(await deriveCelestialBodyAddress(game, currentBody.id)) !==
|
|
860
|
+
stake.body.address) {
|
|
861
|
+
fail(`Celestial Body ${stake.body.address} does not match the Claim Stake's configured Game, canonical Planet identity, or System generation. Refresh the Body and Claim Stake before retrying finalization.`, stake.body.address);
|
|
862
|
+
}
|
|
863
|
+
const system = currentBody.system;
|
|
864
|
+
const player = await deriveStarbasePlayerAddress(system, character);
|
|
865
|
+
const currencyCache = await deriveCurrencyCacheAddress(game);
|
|
866
|
+
const [systemCandidate, playerCandidate, currencyCandidate] = await readContextRawAccounts(ctx, [system, player, currencyCache]);
|
|
867
|
+
const systemAccount = requireAccount(systemCandidate, system, `Star System ${system} is missing. Refresh the Claim Stake's System and retry finalization.`);
|
|
868
|
+
const playerAccount = requireAccount(playerCandidate, player, `Starbase Player ${player} is missing for authorization Profile ${options.authorization.profile}. Register or refresh it in the Claim Stake's System before retrying finalization.`);
|
|
869
|
+
const currencyAccount = requireAccount(currencyCandidate, currencyCache, `Canonical CurrencyCache ${currencyCache} is missing for configured Game ${game}. Refresh the Game currency configuration and retry finalization.`);
|
|
870
|
+
const currentSystem = starSystemAccountDefinition.decode(systemAccount);
|
|
871
|
+
const currentPlayer = starbasePlayerAccountDefinition.decode(playerAccount);
|
|
872
|
+
if (currentSystem.game !== game ||
|
|
873
|
+
currentSystem.sequenceId !== stake.systemSeqId ||
|
|
874
|
+
currentSystem.starbase === undefined ||
|
|
875
|
+
(await deriveStarSystemAddress(game, currentSystem.systemId)) !== system) {
|
|
876
|
+
fail(`Star System ${system} does not match the configured Game, canonical identity, Claim Stake generation, or an active nested Starbase. Refresh the System and retry finalization.`, system);
|
|
877
|
+
}
|
|
878
|
+
if (currentPlayer.playerProfile !== options.authorization.profile ||
|
|
879
|
+
currentPlayer.character !== character ||
|
|
880
|
+
currentPlayer.system !== system) {
|
|
881
|
+
fail(`Starbase Player ${player} does not match the authorization Profile, canonical Character, and Claim Stake System. Refresh it and retry finalization.`, player);
|
|
882
|
+
}
|
|
883
|
+
validateCurrencyCache(currencyAccount, game, currencyCache, 'finalization');
|
|
884
|
+
const description = `Finalize building changes on Claim Stake ${stake.address}.`;
|
|
885
|
+
return createPlan({
|
|
886
|
+
kind: 'claim-stakes.finalize-building-changes',
|
|
887
|
+
preconditions: await Promise.all([
|
|
888
|
+
precondition(freshGame.account, 'The configured Game remains current.'),
|
|
889
|
+
precondition(stakeAccount, `Claim Stake ${stake.address} retains its ownership, design sequence, rent balance, Body, and System generation.`),
|
|
890
|
+
precondition(characterAccount, `The canonical Character remains assigned to Profile ${options.authorization.profile} in the configured Game.`),
|
|
891
|
+
precondition(bodyAccount, `The Claim Stake Planet remains assigned to System ${system} at the current generation.`),
|
|
892
|
+
precondition(systemAccount, `Star System ${currentSystem.name} retains its configured Game, sequence, and nested Starbase.`),
|
|
893
|
+
precondition(playerAccount, `Starbase Player ${player} retains its Profile, Character, and System.`),
|
|
894
|
+
precondition(currencyAccount, 'The canonical CurrencyCache remains assigned to the configured Game.'),
|
|
895
|
+
]),
|
|
896
|
+
steps: [
|
|
897
|
+
{
|
|
898
|
+
describes: description,
|
|
899
|
+
instruction: adaptFinalizeClaimStakeBuildingChangesInstruction({
|
|
900
|
+
authorization: options.authorization,
|
|
901
|
+
celestialBody: stake.body.address,
|
|
902
|
+
character,
|
|
903
|
+
claimStakeInstance: stake.address,
|
|
904
|
+
currencyCache,
|
|
905
|
+
game,
|
|
906
|
+
systemAndStarbasePlayerStarbasePlayer: player,
|
|
907
|
+
systemAndStarbasePlayerSystem: system,
|
|
908
|
+
}),
|
|
909
|
+
signers: [options.authorization.authority],
|
|
910
|
+
},
|
|
911
|
+
],
|
|
912
|
+
summary: description,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
/** Plans cancellation of one owned Claim Stake building design.
|
|
916
|
+
*
|
|
917
|
+
* The planner requires a fresh design state and a coherent configured
|
|
918
|
+
* Game/Claim Stake/Planet/System/Starbase Player graph. The program
|
|
919
|
+
* re-synchronizes rent at execution and returns the Claim Stake to active
|
|
920
|
+
* state; rent arithmetic and concurrent state transitions remain
|
|
921
|
+
* chain-authoritative.
|
|
922
|
+
*
|
|
923
|
+
* @example
|
|
924
|
+
* ```ts
|
|
925
|
+
* declare const ctx: SageContext;
|
|
926
|
+
* declare const stake: ClaimStakeSnapshot;
|
|
927
|
+
* declare const authorization: PlanAuthorization;
|
|
928
|
+
* const plan = await planCancelClaimStakeBuildingChanges(ctx, stake, {
|
|
929
|
+
* authorization,
|
|
930
|
+
* });
|
|
931
|
+
* console.log(plan.describe());
|
|
932
|
+
* ```
|
|
933
|
+
*/
|
|
934
|
+
export async function planCancelClaimStakeBuildingChanges(ctx, stake, input) {
|
|
935
|
+
const options = validateCancelBuildingChangeOptions(input);
|
|
936
|
+
const game = requireGameAddress(ctx);
|
|
937
|
+
if (stake.state.kind !== 'design') {
|
|
938
|
+
fail(`Claim Stake ${stake.address} is not in design state. Refresh it and cancel only a pending building design.`, stake.address);
|
|
939
|
+
}
|
|
940
|
+
if (stake.game !== game) {
|
|
941
|
+
fail(`Claim Stake ${stake.address} does not match the configured Game ${game}. Refresh it through this context and retry.`, stake.address);
|
|
942
|
+
}
|
|
943
|
+
const character = await findCharacterPda(options.authorization.profile, game);
|
|
944
|
+
if (stake.ownerCharacter.address !== character) {
|
|
945
|
+
fail(`Claim Stake ${stake.address} does not belong to authorization Profile ${options.authorization.profile}'s canonical Character ${character}. Choose an owned Claim Stake, refresh it, and retry.`, stake.address);
|
|
946
|
+
}
|
|
947
|
+
const firstAccounts = await readContextRawAccounts(ctx, [
|
|
948
|
+
stake.address,
|
|
949
|
+
stake.body.address,
|
|
950
|
+
]);
|
|
951
|
+
const stakeAccount = requireAccount(firstAccounts[0], stake.address, `Claim Stake ${stake.address} is missing. Refresh it and retry cancellation.`);
|
|
952
|
+
const bodyAccount = requireAccount(firstAccounts[1], stake.body.address, `Celestial Body ${stake.body.address} is missing. Refresh the Claim Stake Body and retry cancellation.`);
|
|
953
|
+
const currentStake = claimStakeAccountDefinition.decode(stakeAccount);
|
|
954
|
+
const currentBody = celestialBodyAccountDefinition.decode(bodyAccount);
|
|
955
|
+
if (currentStake.game !== game ||
|
|
956
|
+
currentStake.body !== stake.body.address ||
|
|
957
|
+
currentStake.ownerCharacter !== character ||
|
|
958
|
+
currentStake.claimStakeDefinitionId !== stake.claimStakeDefinitionId ||
|
|
959
|
+
currentStake.claimStakeDefinitionSeqId !==
|
|
960
|
+
stake.claimStakeDefinitionSeqId ||
|
|
961
|
+
currentStake.buildingDefinitionSeqId !== stake.buildingDefinitionSeqId ||
|
|
962
|
+
currentStake.systemSeqId !== stake.systemSeqId ||
|
|
963
|
+
!sameQuantities(currentStake.buildings, stake.buildings) ||
|
|
964
|
+
currentStake.state.kind !== 'design' ||
|
|
965
|
+
currentStake.state.sequenceId !== stake.state.sequenceId ||
|
|
966
|
+
!sameQuantities(currentStake.state.buildings, stake.state.buildings)) {
|
|
967
|
+
fail(`Claim Stake ${stake.address} changed or does not match its loaded ownership, Body, definitions, buildings, design sequence, or System generation. Refresh it and retry cancellation.`, stake.address);
|
|
968
|
+
}
|
|
969
|
+
if (currentBody.kind !== 'planet' ||
|
|
970
|
+
currentBody.game !== game ||
|
|
971
|
+
currentBody.systemSequenceId !== stake.systemSeqId ||
|
|
972
|
+
(await deriveCelestialBodyAddress(game, currentBody.id)) !==
|
|
973
|
+
stake.body.address) {
|
|
974
|
+
fail(`Celestial Body ${stake.body.address} does not match the Claim Stake's configured Game, canonical Planet identity, or System generation. Refresh the Body and Claim Stake before retrying cancellation.`, stake.body.address);
|
|
975
|
+
}
|
|
976
|
+
const system = currentBody.system;
|
|
977
|
+
const player = await deriveStarbasePlayerAddress(system, character);
|
|
978
|
+
const currencyCache = await deriveCurrencyCacheAddress(game);
|
|
979
|
+
const [systemCandidate, playerCandidate, currencyCandidate] = await readContextRawAccounts(ctx, [system, player, currencyCache]);
|
|
980
|
+
const systemAccount = requireAccount(systemCandidate, system, `Star System ${system} is missing. Refresh the Claim Stake's System and retry cancellation.`);
|
|
981
|
+
const playerAccount = requireAccount(playerCandidate, player, `Starbase Player ${player} is missing for authorization Profile ${options.authorization.profile}. Register or refresh it in the Claim Stake's System before retrying cancellation.`);
|
|
982
|
+
const currencyAccount = requireAccount(currencyCandidate, currencyCache, `Canonical CurrencyCache ${currencyCache} is missing for configured Game ${game}. Refresh the Game currency configuration and retry cancellation.`);
|
|
983
|
+
const currentSystem = starSystemAccountDefinition.decode(systemAccount);
|
|
984
|
+
const currentPlayer = starbasePlayerAccountDefinition.decode(playerAccount);
|
|
985
|
+
if (currentSystem.game !== game ||
|
|
986
|
+
currentSystem.sequenceId !== stake.systemSeqId ||
|
|
987
|
+
currentSystem.starbase === undefined ||
|
|
988
|
+
(await deriveStarSystemAddress(game, currentSystem.systemId)) !== system) {
|
|
989
|
+
fail(`Star System ${system} does not match the configured Game, canonical identity, Claim Stake generation, or an active nested Starbase. Refresh the System and retry cancellation.`, system);
|
|
990
|
+
}
|
|
991
|
+
if (currentPlayer.playerProfile !== options.authorization.profile ||
|
|
992
|
+
currentPlayer.character !== character ||
|
|
993
|
+
currentPlayer.system !== system) {
|
|
994
|
+
fail(`Starbase Player ${player} does not match the authorization Profile, canonical Character, and Claim Stake System. Refresh it and retry cancellation.`, player);
|
|
995
|
+
}
|
|
996
|
+
validateCurrencyCache(currencyAccount, game, currencyCache, 'cancellation');
|
|
997
|
+
const description = `Cancel the pending building design on Claim Stake ${stake.address} and return it to active state.`;
|
|
998
|
+
return createPlan({
|
|
999
|
+
kind: 'claim-stakes.cancel-building-changes',
|
|
1000
|
+
preconditions: await Promise.all([
|
|
1001
|
+
precondition(stakeAccount, `Claim Stake ${stake.address} retains its ownership, design sequence, buildings, Body, and System generation.`),
|
|
1002
|
+
precondition(bodyAccount, `The Claim Stake Planet remains assigned to System ${system} at the current generation.`),
|
|
1003
|
+
precondition(systemAccount, `Star System ${currentSystem.name} retains its configured Game, sequence, and nested Starbase.`),
|
|
1004
|
+
precondition(playerAccount, `Starbase Player ${player} retains its Profile, Character, and System.`),
|
|
1005
|
+
precondition(currencyAccount, 'The canonical CurrencyCache remains assigned to the configured Game.'),
|
|
1006
|
+
]),
|
|
1007
|
+
steps: [
|
|
1008
|
+
{
|
|
1009
|
+
describes: description,
|
|
1010
|
+
instruction: adaptCancelClaimStakeBuildingChangesInstruction({
|
|
1011
|
+
authorization: options.authorization,
|
|
1012
|
+
celestialBody: stake.body.address,
|
|
1013
|
+
claimStakeInstance: stake.address,
|
|
1014
|
+
currencyCache,
|
|
1015
|
+
game,
|
|
1016
|
+
starbasePlayer: player,
|
|
1017
|
+
system,
|
|
1018
|
+
}),
|
|
1019
|
+
signers: [options.authorization.authority],
|
|
1020
|
+
},
|
|
1021
|
+
],
|
|
1022
|
+
summary: description,
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
/** Plans deconstruction of one owned active Claim Stake.
|
|
1026
|
+
*
|
|
1027
|
+
* The planner derives the current Planet chronology index and exact plot bucket
|
|
1028
|
+
* from the Claim Stake definition tier and fresh Body plot matrix. The only
|
|
1029
|
+
* remaining building must be the definition-compatible zero-cost bundled hub,
|
|
1030
|
+
* which deconstruction discards.
|
|
1031
|
+
* Rent settlement, cargo return, account closure, and concurrent races remain
|
|
1032
|
+
* chain-authoritative; the Plan does not estimate a refund.
|
|
1033
|
+
*
|
|
1034
|
+
* @example
|
|
1035
|
+
* ```ts
|
|
1036
|
+
* declare const ctx: SageContext;
|
|
1037
|
+
* declare const stake: ClaimStakeSnapshot;
|
|
1038
|
+
* declare const authorization: PlanAuthorization;
|
|
1039
|
+
* const plan = await planDeconstructClaimStake(ctx, stake, { authorization });
|
|
1040
|
+
* console.log(plan.describe());
|
|
1041
|
+
* ```
|
|
1042
|
+
*/
|
|
1043
|
+
export async function planDeconstructClaimStake(ctx, stake, input) {
|
|
1044
|
+
const options = validateDeconstructOptions(input);
|
|
1045
|
+
const game = requireGameAddress(ctx);
|
|
1046
|
+
if (stake.state.kind !== 'active') {
|
|
1047
|
+
fail(`Claim Stake ${stake.address} is not in active state. Refresh it, finalize or cancel any pending design, and retry deconstruction.`, stake.address);
|
|
1048
|
+
}
|
|
1049
|
+
if (stake.game !== game) {
|
|
1050
|
+
fail(`Claim Stake ${stake.address} does not match the configured Game ${game}. Refresh it through this context and retry.`, stake.address);
|
|
1051
|
+
}
|
|
1052
|
+
const character = await findCharacterPda(options.authorization.profile, game);
|
|
1053
|
+
if (stake.ownerCharacter.address !== character) {
|
|
1054
|
+
fail(`Claim Stake ${stake.address} does not belong to authorization Profile ${options.authorization.profile}'s canonical Character ${character}. Choose an owned Claim Stake, refresh it, and retry.`, stake.address);
|
|
1055
|
+
}
|
|
1056
|
+
const [freshGame, firstAccounts] = await Promise.all([
|
|
1057
|
+
readFreshGameClaimStakeConfiguration(ctx),
|
|
1058
|
+
readContextRawAccounts(ctx, [stake.address, stake.body.address]),
|
|
1059
|
+
]);
|
|
1060
|
+
if (freshGame === undefined) {
|
|
1061
|
+
fail(`Configured Game ${game} is missing. Refresh the Game definitions and retry deconstruction.`, game);
|
|
1062
|
+
}
|
|
1063
|
+
const stakeAccount = requireAccount(firstAccounts[0], stake.address, `Claim Stake ${stake.address} is missing. Refresh it and retry deconstruction.`);
|
|
1064
|
+
const bodyAccount = requireAccount(firstAccounts[1], stake.body.address, `Celestial Body ${stake.body.address} is missing. Refresh the Claim Stake Body and retry deconstruction.`);
|
|
1065
|
+
const currentStake = claimStakeAccountDefinition.decode(stakeAccount);
|
|
1066
|
+
const currentBody = decodeIndexedCelestialBodyAccount(bodyAccount);
|
|
1067
|
+
if (currentStake.game !== game ||
|
|
1068
|
+
currentStake.body !== stake.body.address ||
|
|
1069
|
+
currentStake.ownerCharacter !== character ||
|
|
1070
|
+
currentStake.claimStakeDefinitionId !== stake.claimStakeDefinitionId ||
|
|
1071
|
+
currentStake.claimStakeDefinitionSeqId !==
|
|
1072
|
+
stake.claimStakeDefinitionSeqId ||
|
|
1073
|
+
currentStake.buildingDefinitionSeqId !== stake.buildingDefinitionSeqId ||
|
|
1074
|
+
currentStake.systemSeqId !== stake.systemSeqId ||
|
|
1075
|
+
!sameQuantities(currentStake.buildings, stake.buildings) ||
|
|
1076
|
+
currentStake.state.kind !== 'active') {
|
|
1077
|
+
fail(`Claim Stake ${stake.address} changed or does not match its loaded ownership, Body, definitions, buildings, active state, or System generation. Refresh it and retry deconstruction.`, stake.address);
|
|
1078
|
+
}
|
|
1079
|
+
if (currentBody.kind !== 'planet' ||
|
|
1080
|
+
currentBody.game !== game ||
|
|
1081
|
+
currentBody.systemSequenceId !== stake.systemSeqId ||
|
|
1082
|
+
(await deriveCelestialBodyAddress(game, currentBody.id)) !==
|
|
1083
|
+
stake.body.address) {
|
|
1084
|
+
fail(`Celestial Body ${stake.body.address} does not match the Claim Stake's configured Game, canonical Planet identity, or System generation. Refresh the Body and Claim Stake before retrying deconstruction.`, stake.body.address);
|
|
1085
|
+
}
|
|
1086
|
+
if (freshGame.data.claimStakes.sequenceId !== stake.claimStakeDefinitionSeqId) {
|
|
1087
|
+
fail(`Claim Stake ${stake.address} definition generation is stale against the current Game catalog. Refresh the Claim Stake and definitions to rule out stale local data. If the on-chain generation still differs, use a Claim Stake matching the current catalog before retrying deconstruction.`, stake.address);
|
|
1088
|
+
}
|
|
1089
|
+
if (freshGame.data.buildings.sequenceId !== stake.buildingDefinitionSeqId) {
|
|
1090
|
+
fail(`Claim Stake ${stake.address} building generation is stale against the current Game catalog. Refresh the Claim Stake and definitions to rule out stale local data. If the on-chain generation still differs, use a Claim Stake matching the current catalog before retrying deconstruction.`, stake.address);
|
|
1091
|
+
}
|
|
1092
|
+
const claimDefinition = freshGame.data.claimStakes.definitions.find(({ id }) => id === stake.claimStakeDefinitionId);
|
|
1093
|
+
if (claimDefinition === undefined) {
|
|
1094
|
+
fail(`Claim Stake definition ${String(stake.claimStakeDefinitionId)} is absent from the current Game catalog. Refresh the Claim Stake and definitions before retrying deconstruction.`, game);
|
|
1095
|
+
}
|
|
1096
|
+
const definitions = new Map(freshGame.data.buildings.definitions.map((definition) => [
|
|
1097
|
+
definition.id,
|
|
1098
|
+
definition,
|
|
1099
|
+
]));
|
|
1100
|
+
const bundledHub = stake.buildings.length === 1 && stake.buildings[0]?.quantity === 1
|
|
1101
|
+
? definitions.get(stake.buildings[0].buildingId)
|
|
1102
|
+
: undefined;
|
|
1103
|
+
if (bundledHub === undefined ||
|
|
1104
|
+
bundledHub.hubValue !== claimDefinition.hubValue ||
|
|
1105
|
+
bundledHub.minimumTier !== claimDefinition.tier ||
|
|
1106
|
+
bundledHub.constructionCost.length !== 0) {
|
|
1107
|
+
const hasPlayerFundedBuilding = stake.buildings.some((building) => {
|
|
1108
|
+
const definition = definitions.get(building.buildingId);
|
|
1109
|
+
return (definition === undefined ||
|
|
1110
|
+
definition.hubValue !== claimDefinition.hubValue ||
|
|
1111
|
+
definition.minimumTier !== claimDefinition.tier ||
|
|
1112
|
+
definition.constructionCost.length !== 0);
|
|
1113
|
+
});
|
|
1114
|
+
fail(hasPlayerFundedBuilding
|
|
1115
|
+
? `Claim Stake ${stake.address} still contains a player-funded building. Remove and finalize every player-funded building so only its verified bundled hub remains before retrying deconstruction.`
|
|
1116
|
+
: `Claim Stake ${stake.address} does not contain exactly one verified bundled hub. Refresh it and restore the definition-compatible bundled hub before retrying deconstruction.`, stake.address);
|
|
1117
|
+
}
|
|
1118
|
+
const groups = currentBody.details.claimedPlots.filter(({ key }) => key === claimDefinition.id);
|
|
1119
|
+
if (groups.length !== 1) {
|
|
1120
|
+
fail(`Claim Stake definition ${String(claimDefinition.id)} does not have exactly one coherent group in Planet ${stake.body.address}'s Claim Stake Body data. Refresh the Body before retrying deconstruction.`, stake.body.address);
|
|
1121
|
+
}
|
|
1122
|
+
const group = groups[0];
|
|
1123
|
+
const chronologyMatches = group.chronoAddresses
|
|
1124
|
+
.map((address, index) => ({ address, index }))
|
|
1125
|
+
.filter(({ address }) => address === stake.address);
|
|
1126
|
+
if (chronologyMatches.length !== 1) {
|
|
1127
|
+
fail(`Claim Stake ${stake.address} must appear exactly once in the Planet chronology before its deconstruction index can be derived. Refresh the Body and retry.`, stake.body.address);
|
|
1128
|
+
}
|
|
1129
|
+
const activeStructures = group.structures.filter(({ address, deactivationTimeUnixSeconds }) => address === stake.address && deactivationTimeUnixSeconds === 0n);
|
|
1130
|
+
if (activeStructures.length !== 1) {
|
|
1131
|
+
fail(`Claim Stake ${stake.address} is not represented exactly once as an active structure in the Planet Body data. Refresh the Body and retry deconstruction.`, stake.body.address);
|
|
1132
|
+
}
|
|
1133
|
+
const claimStakeIndex = chronologyMatches[0].index;
|
|
1134
|
+
const tierIndex = TIER_INDEX[claimDefinition.tier];
|
|
1135
|
+
const plotCapacities = currentBody.details.claimStakePlots.map((row) => row[tierIndex]);
|
|
1136
|
+
if (plotCapacities.some((capacity, index) => index > 0 && capacity < plotCapacities[index - 1])) {
|
|
1137
|
+
fail(`Planet ${stake.body.address} Claim Stake plot capacity is not coherent across Starbase levels for definition tier ${claimDefinition.tier}. Refresh the Body and verify the pinned binding version before retrying deconstruction.`, stake.body.address);
|
|
1138
|
+
}
|
|
1139
|
+
if (group.chronoAddresses.length > plotCapacities[plotCapacities.length - 1]) {
|
|
1140
|
+
fail(`Planet ${stake.body.address} Claim Stake plot capacity does not cover its definition-group chronology. Refresh the Body before retrying deconstruction.`, stake.body.address);
|
|
1141
|
+
}
|
|
1142
|
+
const plotLevelIndex = plotCapacities.findIndex((capacity) => claimStakeIndex < capacity);
|
|
1143
|
+
const claimStakeLevel = CLAIM_STAKE_PLOT_LEVELS[plotLevelIndex];
|
|
1144
|
+
const system = currentBody.system;
|
|
1145
|
+
const player = await deriveStarbasePlayerAddress(system, character);
|
|
1146
|
+
const [characterCandidate, systemCandidate, playerCandidate] = await readContextRawAccounts(ctx, [character, system, player]);
|
|
1147
|
+
const characterAccount = requireAccount(characterCandidate, character, `Canonical Character ${character} is missing. Refresh the Profile Character and retry deconstruction.`);
|
|
1148
|
+
const systemAccount = requireAccount(systemCandidate, system, `Star System ${system} is missing. Refresh the Claim Stake's System and retry deconstruction.`);
|
|
1149
|
+
const playerAccount = requireAccount(playerCandidate, player, `Starbase Player ${player} is missing for authorization Profile ${options.authorization.profile}. Register or refresh it in the Claim Stake's System before retrying deconstruction.`);
|
|
1150
|
+
const currentCharacter = characterAccountDefinition.decode(characterAccount);
|
|
1151
|
+
const currentSystem = starSystemAccountDefinition.decode(systemAccount);
|
|
1152
|
+
const currentPlayer = starbasePlayerAccountDefinition.decode(playerAccount);
|
|
1153
|
+
if (currentCharacter.playerProfile !== options.authorization.profile ||
|
|
1154
|
+
currentCharacter.game !== game) {
|
|
1155
|
+
fail(`Character ${character} does not match authorization Profile ${options.authorization.profile} and configured Game ${game}. Refresh the Character and retry deconstruction.`, character);
|
|
1156
|
+
}
|
|
1157
|
+
if (currentSystem.game !== game ||
|
|
1158
|
+
currentSystem.sequenceId !== stake.systemSeqId ||
|
|
1159
|
+
currentSystem.starbase === undefined ||
|
|
1160
|
+
!currentSystem.celestialBodyAddresses.includes(stake.body.address) ||
|
|
1161
|
+
(await deriveStarSystemAddress(game, currentSystem.systemId)) !== system) {
|
|
1162
|
+
fail(`Star System ${system} does not match the configured Game, canonical identity, Claim Stake generation, or an active nested Starbase. Refresh the System and retry deconstruction.`, system);
|
|
1163
|
+
}
|
|
1164
|
+
if (currentPlayer.playerProfile !== options.authorization.profile ||
|
|
1165
|
+
currentPlayer.character !== character ||
|
|
1166
|
+
currentPlayer.system !== system) {
|
|
1167
|
+
fail(`Starbase Player ${player} does not match the authorization Profile, canonical Character, and Claim Stake System. Refresh it and retry deconstruction.`, player);
|
|
1168
|
+
}
|
|
1169
|
+
const description = `Deconstruct Claim Stake ${stake.address} and discard its bundled hub ${bundledHub.name}; rent settlement, cargo return, account closure, and any refund remain chain-authoritative.`;
|
|
1170
|
+
return createPlan({
|
|
1171
|
+
kind: 'claim-stakes.deconstruct',
|
|
1172
|
+
preconditions: await Promise.all([
|
|
1173
|
+
precondition(freshGame.account, `The configured Game retains Claim Stake definition ${String(claimDefinition.id)}, bundled hub ${String(bundledHub.id)}, and their current generations.`),
|
|
1174
|
+
precondition(stakeAccount, `Claim Stake ${stake.address} retains its ownership, active state, definitions, bundled hub, Body, and System generation.`),
|
|
1175
|
+
precondition(bodyAccount, `The Planet retains Claim Stake ${stake.address} at chronology index ${String(claimStakeIndex)} in definition group ${String(claimDefinition.id)}.`),
|
|
1176
|
+
precondition(characterAccount, `The canonical Character remains assigned to Profile ${options.authorization.profile} in the configured Game.`),
|
|
1177
|
+
precondition(systemAccount, `Star System ${currentSystem.name} retains its configured Game, sequence, nested Starbase, and Claim Stake Body relationship.`),
|
|
1178
|
+
precondition(playerAccount, `Starbase Player ${player} retains its Profile, Character, and System.`),
|
|
1179
|
+
]),
|
|
1180
|
+
steps: [
|
|
1181
|
+
{
|
|
1182
|
+
describes: description,
|
|
1183
|
+
instruction: adaptDeconstructClaimStakeInstruction({
|
|
1184
|
+
authorization: options.authorization,
|
|
1185
|
+
celestialBody: stake.body.address,
|
|
1186
|
+
character,
|
|
1187
|
+
claimStakeIndex,
|
|
1188
|
+
claimStakeInstance: stake.address,
|
|
1189
|
+
claimStakeLevel,
|
|
1190
|
+
game,
|
|
1191
|
+
systemAndStarbasePlayerStarbasePlayer: player,
|
|
1192
|
+
systemAndStarbasePlayerSystem: system,
|
|
1193
|
+
}),
|
|
1194
|
+
signers: [options.authorization.authority],
|
|
1195
|
+
},
|
|
1196
|
+
],
|
|
1197
|
+
summary: description,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
/** Plans one exact raw rent top-up for an owned placed Claim Stake.
|
|
1201
|
+
*
|
|
1202
|
+
* The planner validates the fresh Claim Stake and its canonical
|
|
1203
|
+
* Profile/Character/Planet/System/Starbase Player graph, plus the Character's
|
|
1204
|
+
* known raw ATLAS balance. Permission checks, concurrent balance changes, rent
|
|
1205
|
+
* arithmetic, and duration purchased remain chain-authoritative.
|
|
1206
|
+
*
|
|
1207
|
+
* @example
|
|
1208
|
+
* ```ts
|
|
1209
|
+
* declare const ctx: SageContext;
|
|
1210
|
+
* declare const stake: ClaimStakeSnapshot;
|
|
1211
|
+
* declare const authorization: PlanAuthorization;
|
|
1212
|
+
* const plan = await planTopUpClaimStakeRent(ctx, stake, {
|
|
1213
|
+
* amount: 500_000_000n,
|
|
1214
|
+
* authorization,
|
|
1215
|
+
* });
|
|
1216
|
+
* console.log(plan.describe());
|
|
1217
|
+
* ```
|
|
1218
|
+
*/
|
|
1219
|
+
export async function planTopUpClaimStakeRent(ctx, stake, input) {
|
|
1220
|
+
const options = validateTopUpRentOptions(input);
|
|
1221
|
+
const game = requireGameAddress(ctx);
|
|
1222
|
+
const loadedState = stake.state;
|
|
1223
|
+
if (loadedState.kind === 'deactivated') {
|
|
1224
|
+
fail(`Claim Stake ${stake.address} is not placed. Refresh it and choose an active Claim Stake or pending building design before topping up rent.`, stake.address);
|
|
1225
|
+
}
|
|
1226
|
+
if (stake.game !== game) {
|
|
1227
|
+
fail(`Claim Stake ${stake.address} does not match the configured Game ${game}. Refresh it through this context and retry.`, stake.address);
|
|
1228
|
+
}
|
|
1229
|
+
const character = await findCharacterPda(options.authorization.profile, game);
|
|
1230
|
+
if (stake.ownerCharacter.address !== character) {
|
|
1231
|
+
fail(`Claim Stake ${stake.address} does not belong to authorization Profile ${options.authorization.profile}'s canonical Character ${character}. Choose an owned Claim Stake, refresh it, and retry.`, stake.address);
|
|
1232
|
+
}
|
|
1233
|
+
const firstAccounts = await readContextRawAccounts(ctx, [
|
|
1234
|
+
stake.address,
|
|
1235
|
+
stake.body.address,
|
|
1236
|
+
character,
|
|
1237
|
+
]);
|
|
1238
|
+
const stakeAccount = requireAccount(firstAccounts[0], stake.address, `Claim Stake ${stake.address} is missing. Refresh it and retry the rent top-up.`);
|
|
1239
|
+
const bodyAccount = requireAccount(firstAccounts[1], stake.body.address, `Celestial Body ${stake.body.address} is missing. Refresh the Claim Stake Body and retry the rent top-up.`);
|
|
1240
|
+
const characterAccount = requireAccount(firstAccounts[2], character, `Canonical Character ${character} is missing. Refresh the Profile Character and retry the rent top-up.`);
|
|
1241
|
+
const currentStake = claimStakeAccountDefinition.decode(stakeAccount);
|
|
1242
|
+
const currentBody = celestialBodyAccountDefinition.decode(bodyAccount);
|
|
1243
|
+
const currentCharacter = characterAccountDefinition.decode(characterAccount);
|
|
1244
|
+
if (currentStake.game !== game ||
|
|
1245
|
+
currentStake.body !== stake.body.address ||
|
|
1246
|
+
currentStake.ownerCharacter !== character ||
|
|
1247
|
+
currentStake.claimStakeDefinitionId !== stake.claimStakeDefinitionId ||
|
|
1248
|
+
currentStake.claimStakeDefinitionSeqId !==
|
|
1249
|
+
stake.claimStakeDefinitionSeqId ||
|
|
1250
|
+
currentStake.buildingDefinitionSeqId !== stake.buildingDefinitionSeqId ||
|
|
1251
|
+
currentStake.systemSeqId !== stake.systemSeqId ||
|
|
1252
|
+
currentStake.rentBalanceRaw !== stake.rentBalanceRaw ||
|
|
1253
|
+
!sameQuantities(currentStake.buildings, stake.buildings) ||
|
|
1254
|
+
!sameStakeState(currentStake.state, loadedState)) {
|
|
1255
|
+
fail(`Claim Stake ${stake.address} changed or does not match its loaded ownership, Body, definitions, buildings, rent balance, placed state, or System generation. Refresh it and retry the rent top-up.`, stake.address);
|
|
1256
|
+
}
|
|
1257
|
+
if (currentBody.kind !== 'planet' ||
|
|
1258
|
+
currentBody.game !== game ||
|
|
1259
|
+
currentBody.systemSequenceId !== stake.systemSeqId ||
|
|
1260
|
+
(await deriveCelestialBodyAddress(game, currentBody.id)) !==
|
|
1261
|
+
stake.body.address) {
|
|
1262
|
+
fail(`Celestial Body ${stake.body.address} does not match the Claim Stake's configured Game, canonical Planet identity, or System generation. Refresh the Body and Claim Stake before retrying the rent top-up.`, stake.body.address);
|
|
1263
|
+
}
|
|
1264
|
+
if (currentCharacter.playerProfile !== options.authorization.profile ||
|
|
1265
|
+
currentCharacter.game !== game) {
|
|
1266
|
+
fail(`Character ${character} does not match authorization Profile ${options.authorization.profile} and configured Game ${game}. Refresh the Character and retry the rent top-up.`, character);
|
|
1267
|
+
}
|
|
1268
|
+
if (currentCharacter.atlasRaw < options.amount) {
|
|
1269
|
+
fail(`Character ${character} has only ${String(currentCharacter.atlasRaw)} known raw ATLAS, below the requested ${String(options.amount)} raw rent top-up. Reduce the amount or add ATLAS, refresh the Character, and retry.`, character);
|
|
1270
|
+
}
|
|
1271
|
+
const system = currentBody.system;
|
|
1272
|
+
const player = await deriveStarbasePlayerAddress(system, character);
|
|
1273
|
+
const [systemCandidate, playerCandidate] = await readContextRawAccounts(ctx, [
|
|
1274
|
+
system,
|
|
1275
|
+
player,
|
|
1276
|
+
]);
|
|
1277
|
+
const systemAccount = requireAccount(systemCandidate, system, `Star System ${system} is missing. Refresh the Claim Stake's System and retry the rent top-up.`);
|
|
1278
|
+
const playerAccount = requireAccount(playerCandidate, player, `Starbase Player ${player} is missing for authorization Profile ${options.authorization.profile}. Register or refresh it in the Claim Stake's System before retrying the rent top-up.`);
|
|
1279
|
+
const currentSystem = starSystemAccountDefinition.decode(systemAccount);
|
|
1280
|
+
const currentPlayer = starbasePlayerAccountDefinition.decode(playerAccount);
|
|
1281
|
+
if (currentSystem.game !== game ||
|
|
1282
|
+
currentSystem.sequenceId !== stake.systemSeqId ||
|
|
1283
|
+
currentSystem.starbase === undefined ||
|
|
1284
|
+
!currentSystem.celestialBodyAddresses.includes(stake.body.address) ||
|
|
1285
|
+
(await deriveStarSystemAddress(game, currentSystem.systemId)) !== system) {
|
|
1286
|
+
fail(`Star System ${system} does not match the configured Game, canonical identity, Claim Stake generation, active nested Starbase, or Claim Stake Body relationship. Refresh the System and retry the rent top-up.`, system);
|
|
1287
|
+
}
|
|
1288
|
+
if (currentPlayer.playerProfile !== options.authorization.profile ||
|
|
1289
|
+
currentPlayer.character !== character ||
|
|
1290
|
+
currentPlayer.system !== system) {
|
|
1291
|
+
fail(`Starbase Player ${player} does not match the authorization Profile, canonical Character, and Claim Stake System. Refresh it and retry the rent top-up.`, player);
|
|
1292
|
+
}
|
|
1293
|
+
const description = `Top up Claim Stake ${stake.address} rent with ${String(options.amount)} raw rent units; rent arithmetic remains chain-authoritative.`;
|
|
1294
|
+
return createPlan({
|
|
1295
|
+
kind: 'claim-stakes.top-up-rent',
|
|
1296
|
+
preconditions: await Promise.all([
|
|
1297
|
+
precondition(stakeAccount, `Claim Stake ${stake.address} retains its ownership, rent balance, placed state, Body, and System generation.`),
|
|
1298
|
+
precondition(bodyAccount, `The Claim Stake Planet remains assigned to System ${system} at the current generation.`),
|
|
1299
|
+
precondition(characterAccount, `The canonical Character remains assigned to Profile ${options.authorization.profile} with at least ${String(options.amount)} known raw ATLAS.`),
|
|
1300
|
+
precondition(systemAccount, `Star System ${currentSystem.name} retains its configured Game, sequence, nested Starbase, and Claim Stake Body relationship.`),
|
|
1301
|
+
precondition(playerAccount, `Starbase Player ${player} retains its Profile, Character, and System.`),
|
|
1302
|
+
]),
|
|
1303
|
+
steps: [
|
|
1304
|
+
{
|
|
1305
|
+
describes: description,
|
|
1306
|
+
instruction: adaptTopUpClaimStakeRentInstruction({
|
|
1307
|
+
amount: options.amount,
|
|
1308
|
+
authorization: options.authorization,
|
|
1309
|
+
celestialBody: stake.body.address,
|
|
1310
|
+
character,
|
|
1311
|
+
claimStakeInstance: stake.address,
|
|
1312
|
+
game,
|
|
1313
|
+
systemAndStarbasePlayerStarbasePlayer: player,
|
|
1314
|
+
systemAndStarbasePlayerSystem: system,
|
|
1315
|
+
}),
|
|
1316
|
+
signers: [options.authorization.authority],
|
|
1317
|
+
},
|
|
1318
|
+
],
|
|
1319
|
+
summary: description,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
function cargoAmount(values, cargoId) {
|
|
1323
|
+
return values.find((item) => item.cargoId === cargoId)?.amount ?? 0n;
|
|
1324
|
+
}
|
|
1325
|
+
function sameCargoAmounts(left, right) {
|
|
1326
|
+
const leftById = new Map(left.map(({ amount, cargoId }) => [cargoId, amount]));
|
|
1327
|
+
const rightById = new Map(right.map(({ amount, id }) => [id, amount]));
|
|
1328
|
+
return (leftById.size === left.length &&
|
|
1329
|
+
rightById.size === right.length &&
|
|
1330
|
+
leftById.size === rightById.size &&
|
|
1331
|
+
[...leftById].every(([id, amount]) => rightById.get(id) === amount));
|
|
1332
|
+
}
|
|
1333
|
+
function requireCargoBalance(source, address, cargoId, available, requested) {
|
|
1334
|
+
if (available < requested) {
|
|
1335
|
+
fail(`${source} has ${String(available)} raw units of cargo id ${String(cargoId)}, but this transfer requests ${String(requested)}. Reduce the request, add cargo, refresh both snapshots, and retry.`, address);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function requireTransferCapacity(destination, address, used, requested, total) {
|
|
1339
|
+
if (used + requested > total) {
|
|
1340
|
+
const remaining = used < total ? total - used : 0n;
|
|
1341
|
+
fail(`${destination} has ${String(remaining)} raw capacity units remaining, but this transfer requires ${String(requested)}. Reduce the request or free capacity, refresh both snapshots, and retry.`, address);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function fleetCargoAmount(fleet, cargoId) {
|
|
1345
|
+
return ((fleet.ammoBank.cargoId === cargoId ? fleet.ammoBank.amount : 0n) +
|
|
1346
|
+
(fleet.fuelTank.cargoId === cargoId ? fleet.fuelTank.amount : 0n) +
|
|
1347
|
+
cargoAmount(fleet.cargoHold.amounts, cargoId));
|
|
1348
|
+
}
|
|
1349
|
+
function describeCargoRequests(direction, requests) {
|
|
1350
|
+
return requests.map(({ amount, cargoId }) => `${direction} cargo id ${String(cargoId)} = ${String(amount)} raw ${amount === 1n ? 'unit' : 'units'}`);
|
|
1351
|
+
}
|
|
1352
|
+
/** Plans the start of one Fleet transfer to an owned active Claim Stake.
|
|
1353
|
+
*
|
|
1354
|
+
* The planner validates the shared Profile/Game, idle Fleet with normalized
|
|
1355
|
+
* crew, active Claim Stake, canonical Planet/System/Starbase Player graph,
|
|
1356
|
+
* exact source balances, exact destination capacities, and an optional present
|
|
1357
|
+
* FleetCrewBinding. The chain remains authoritative for range, duration,
|
|
1358
|
+
* permission bits, concurrent state changes, and transfer arithmetic.
|
|
1359
|
+
*
|
|
1360
|
+
* @example
|
|
1361
|
+
* ```ts
|
|
1362
|
+
* declare const ctx: SageContext;
|
|
1363
|
+
* declare const fleet: FleetSnapshot;
|
|
1364
|
+
* declare const stake: ClaimStakeSnapshot;
|
|
1365
|
+
* declare const authorization: PlanAuthorization;
|
|
1366
|
+
* const plan = await planStartClaimStakeFleetTransfer(ctx, fleet, stake, {
|
|
1367
|
+
* authorization,
|
|
1368
|
+
* load: [{ cargoId: 3, amount: 25n }],
|
|
1369
|
+
* unload: [{ cargoId: 1, amount: 10n }],
|
|
1370
|
+
* });
|
|
1371
|
+
* console.log(plan.describe());
|
|
1372
|
+
* ```
|
|
1373
|
+
*/
|
|
1374
|
+
export async function planStartClaimStakeFleetTransfer(ctx, fleet, stake, input) {
|
|
1375
|
+
const options = validateStartFleetTransferOptions(input);
|
|
1376
|
+
const game = requireGameAddress(ctx);
|
|
1377
|
+
if (fleet.game !== game || stake.game !== game) {
|
|
1378
|
+
fail(`Fleet ${fleet.address} and Claim Stake ${stake.address} must both belong to configured Game ${game}. Refresh both snapshots through this context and retry.`, fleet.game !== game ? fleet.address : stake.address);
|
|
1379
|
+
}
|
|
1380
|
+
if (fleet.ownerProfile.address !== options.authorization.profile) {
|
|
1381
|
+
fail(`Fleet ${fleet.name} belongs to Profile ${fleet.ownerProfile.address}, not authorization Profile ${options.authorization.profile}. Use the owning Profile, refresh the Fleet, and retry.`, fleet.address);
|
|
1382
|
+
}
|
|
1383
|
+
if (fleet.state.kind !== 'idle') {
|
|
1384
|
+
fail(`Fleet ${fleet.name} is ${fleet.state.kind}, but starting a Claim Stake transfer requires an idle Fleet. Return it to idle state, refresh it, and retry.`, fleet.address);
|
|
1385
|
+
}
|
|
1386
|
+
if (!Number.isSafeInteger(fleet.crewCount) || fleet.crewCount <= 0) {
|
|
1387
|
+
fail(`Fleet ${fleet.name} has no valid normalized crew and cannot start a Claim Stake transfer. Crew the Fleet, refresh it, and retry.`, fleet.address);
|
|
1388
|
+
}
|
|
1389
|
+
if (stake.state.kind !== 'active') {
|
|
1390
|
+
fail(`Claim Stake ${stake.address} is ${stake.state.kind}, but starting a Fleet transfer requires an active Claim Stake. Activate or restore it, refresh it, and retry.`, stake.address);
|
|
1391
|
+
}
|
|
1392
|
+
const character = await findCharacterPda(options.authorization.profile, game);
|
|
1393
|
+
if (stake.ownerCharacter.address !== character) {
|
|
1394
|
+
fail(`Claim Stake ${stake.address} does not belong to authorization Profile ${options.authorization.profile}'s canonical Character ${character}. Choose an owned active Claim Stake, refresh it, and retry.`, stake.address);
|
|
1395
|
+
}
|
|
1396
|
+
const [fleetCandidate, stakeCandidate, bodyCandidate, characterCandidate] = await readContextRawAccounts(ctx, [
|
|
1397
|
+
fleet.address,
|
|
1398
|
+
stake.address,
|
|
1399
|
+
stake.body.address,
|
|
1400
|
+
character,
|
|
1401
|
+
]);
|
|
1402
|
+
const fleetAccount = requireAccount(fleetCandidate, fleet.address, `Fleet ${fleet.name} is missing. Refresh it and retry the Claim Stake transfer.`);
|
|
1403
|
+
const stakeAccount = requireAccount(stakeCandidate, stake.address, `Claim Stake ${stake.address} is missing. Refresh it and retry the Fleet transfer.`);
|
|
1404
|
+
const bodyAccount = requireAccount(bodyCandidate, stake.body.address, `Celestial Body ${stake.body.address} is missing. Refresh the Claim Stake Body and retry the Fleet transfer.`);
|
|
1405
|
+
const characterAccount = requireAccount(characterCandidate, character, `Canonical Character ${character} is missing. Refresh the Profile Character and retry the Claim Stake transfer.`);
|
|
1406
|
+
const currentFleet = fleetAccountDefinition.decode(fleetAccount);
|
|
1407
|
+
const currentStake = claimStakeAccountDefinition.decode(stakeAccount);
|
|
1408
|
+
const currentBody = celestialBodyAccountDefinition.decode(bodyAccount);
|
|
1409
|
+
const currentCharacter = characterAccountDefinition.decode(characterAccount);
|
|
1410
|
+
if (!Number.isSafeInteger(currentFleet.crewCount) ||
|
|
1411
|
+
currentFleet.crewCount <= 0) {
|
|
1412
|
+
fail(`Fleet ${fleet.name} has no valid normalized crew and cannot start a Claim Stake transfer. Crew the Fleet, refresh it, and retry.`, fleet.address);
|
|
1413
|
+
}
|
|
1414
|
+
if (currentFleet.game !== game ||
|
|
1415
|
+
currentFleet.ownerProfile !== options.authorization.profile ||
|
|
1416
|
+
currentFleet.name !== fleet.name ||
|
|
1417
|
+
currentFleet.state.kind !== 'idle' ||
|
|
1418
|
+
currentFleet.crewCount !== fleet.crewCount ||
|
|
1419
|
+
(await deriveFleetAddress(currentFleet)) !== fleet.address ||
|
|
1420
|
+
currentFleet.ammoBank.cargoId !== fleet.ammo.id ||
|
|
1421
|
+
currentFleet.ammoBank.amount !== fleet.ammo.amount ||
|
|
1422
|
+
currentFleet.fuelTank.cargoId !== fleet.fuel.id ||
|
|
1423
|
+
currentFleet.fuelTank.amount !== fleet.fuel.amount ||
|
|
1424
|
+
currentFleet.cargoHold.storageCost !== fleet.cargoHold.storageCost ||
|
|
1425
|
+
!sameCargoAmounts(currentFleet.cargoHold.amounts, fleet.cargoHold.items) ||
|
|
1426
|
+
currentFleet.stats.cargo.ammoCapacity !== fleet.capacities.ammo.total ||
|
|
1427
|
+
currentFleet.stats.cargo.fuelCapacity !== fleet.capacities.fuel.total ||
|
|
1428
|
+
currentFleet.stats.cargo.cargoCapacity !== fleet.capacities.cargo.total) {
|
|
1429
|
+
fail(`Fleet ${fleet.name} changed or no longer matches its loaded Game, owner, canonical address, name, idle state, normalized crew, cargo, or capacities. Refresh it and create a new Claim Stake transfer Plan.`, fleet.address);
|
|
1430
|
+
}
|
|
1431
|
+
if (currentStake.game !== game ||
|
|
1432
|
+
currentStake.ownerCharacter !== character ||
|
|
1433
|
+
currentStake.body !== stake.body.address ||
|
|
1434
|
+
currentStake.systemSeqId !== stake.systemSeqId ||
|
|
1435
|
+
currentStake.state.kind !== 'active' ||
|
|
1436
|
+
currentStake.production.cargoPod.storageCost !==
|
|
1437
|
+
stake.resources.capacity.usedRaw ||
|
|
1438
|
+
currentStake.production.resourceCapacityRaw !==
|
|
1439
|
+
stake.resources.capacity.totalRaw ||
|
|
1440
|
+
!sameCargoAmounts(currentStake.production.cargoPod.amounts, stake.resources.inventory.items.map(({ id, quantityRaw }) => ({
|
|
1441
|
+
amount: quantityRaw,
|
|
1442
|
+
id,
|
|
1443
|
+
})))) {
|
|
1444
|
+
fail(`Claim Stake ${stake.address} changed or no longer matches its loaded Game, owner, Body, active state, resources, capacity, or System generation. Refresh it and create a new Fleet transfer Plan.`, stake.address);
|
|
1445
|
+
}
|
|
1446
|
+
if (currentBody.kind !== 'planet' ||
|
|
1447
|
+
currentBody.game !== game ||
|
|
1448
|
+
currentBody.systemSequenceId !== stake.systemSeqId ||
|
|
1449
|
+
(await deriveCelestialBodyAddress(game, currentBody.id)) !==
|
|
1450
|
+
stake.body.address) {
|
|
1451
|
+
fail(`Celestial Body ${stake.body.address} does not match the Claim Stake's configured Game, canonical Planet identity, or System generation. Refresh the Body and Claim Stake before retrying the Fleet transfer.`, stake.body.address);
|
|
1452
|
+
}
|
|
1453
|
+
if (currentCharacter.playerProfile !== options.authorization.profile ||
|
|
1454
|
+
currentCharacter.game !== game) {
|
|
1455
|
+
fail(`Character ${character} does not match authorization Profile ${options.authorization.profile} and configured Game ${game}. Refresh the Character and retry the Claim Stake transfer.`, character);
|
|
1456
|
+
}
|
|
1457
|
+
const system = currentBody.system;
|
|
1458
|
+
const player = await deriveStarbasePlayerAddress(system, character);
|
|
1459
|
+
const currencyCache = await deriveCurrencyCacheAddress(game);
|
|
1460
|
+
const crewBinding = await deriveFleetCrewBindingAddress(fleet.address);
|
|
1461
|
+
const [systemCandidate, playerCandidate, currencyCandidate, crewCandidate] = await readContextRawAccounts(ctx, [
|
|
1462
|
+
system,
|
|
1463
|
+
player,
|
|
1464
|
+
currencyCache,
|
|
1465
|
+
crewBinding,
|
|
1466
|
+
]);
|
|
1467
|
+
const systemAccount = requireAccount(systemCandidate, system, `Star System ${system} is missing. Refresh the Claim Stake System and retry the Fleet transfer.`);
|
|
1468
|
+
const playerAccount = requireAccount(playerCandidate, player, `Starbase Player ${player} is missing for authorization Profile ${options.authorization.profile}. Register or refresh it in the Claim Stake System before retrying the Fleet transfer.`);
|
|
1469
|
+
const currencyAccount = requireAccount(currencyCandidate, currencyCache, `Canonical CurrencyCache ${currencyCache} is missing for configured Game ${game}. Refresh the Game currency configuration and retry the Fleet transfer.`);
|
|
1470
|
+
const currentSystem = starSystemAccountDefinition.decode(systemAccount);
|
|
1471
|
+
const currentPlayer = starbasePlayerAccountDefinition.decode(playerAccount);
|
|
1472
|
+
if (currentSystem.game !== game ||
|
|
1473
|
+
currentSystem.sequenceId !== stake.systemSeqId ||
|
|
1474
|
+
currentSystem.starbase === undefined ||
|
|
1475
|
+
!currentSystem.celestialBodyAddresses.includes(stake.body.address) ||
|
|
1476
|
+
(await deriveStarSystemAddress(game, currentSystem.systemId)) !== system) {
|
|
1477
|
+
fail(`Star System ${system} does not match the configured Game, canonical identity, Claim Stake generation, nested Starbase, or Claim Stake Body relationship. Refresh the System and retry the Fleet transfer.`, system);
|
|
1478
|
+
}
|
|
1479
|
+
if (currentPlayer.playerProfile !== options.authorization.profile ||
|
|
1480
|
+
currentPlayer.character !== character ||
|
|
1481
|
+
currentPlayer.system !== system) {
|
|
1482
|
+
fail(`Starbase Player ${player} does not match the authorization Profile, canonical Character, and Claim Stake System. Refresh it and retry the Fleet transfer.`, player);
|
|
1483
|
+
}
|
|
1484
|
+
validateCurrencyCache(currencyAccount, game, currencyCache, 'the Claim Stake Fleet transfer');
|
|
1485
|
+
if (crewCandidate !== undefined) {
|
|
1486
|
+
const currentCrewBinding = fleetCrewBindingAccountDefinition.decode(crewCandidate);
|
|
1487
|
+
if (crewCandidate.address !== crewBinding ||
|
|
1488
|
+
currentCrewBinding.fleet !== fleet.address ||
|
|
1489
|
+
currentCrewBinding.ownerProfile !== options.authorization.profile) {
|
|
1490
|
+
fail(`FleetCrewBinding ${crewCandidate.address} does not match Fleet ${fleet.address}, its owner Profile, or its canonical PDA. Refresh the Fleet crew registration or remove the invalid binding before retrying.`, crewCandidate.address);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const definitions = new Map(await Promise.all([...options.load, ...options.unload].map(async (request) => {
|
|
1494
|
+
const definition = await resolveCargo(ctx, request.cargoId);
|
|
1495
|
+
return [request.cargoId, definition];
|
|
1496
|
+
})));
|
|
1497
|
+
for (const request of options.load) {
|
|
1498
|
+
requireCargoBalance(`Claim Stake ${stake.address}`, stake.address, request.cargoId, cargoAmount(currentStake.production.cargoPod.amounts, request.cargoId), request.amount);
|
|
1499
|
+
}
|
|
1500
|
+
for (const request of options.unload) {
|
|
1501
|
+
requireCargoBalance(`Fleet ${fleet.name}`, fleet.address, request.cargoId, fleetCargoAmount(currentFleet, request.cargoId), request.amount);
|
|
1502
|
+
}
|
|
1503
|
+
const loadAmmo = options.load
|
|
1504
|
+
.filter(({ cargoId }) => cargoId === currentFleet.ammoBank.cargoId)
|
|
1505
|
+
.reduce((total, { amount }) => total + amount, 0n);
|
|
1506
|
+
const loadFuel = options.load
|
|
1507
|
+
.filter(({ cargoId }) => cargoId === currentFleet.fuelTank.cargoId)
|
|
1508
|
+
.reduce((total, { amount }) => total + amount, 0n);
|
|
1509
|
+
const loadCargoStorage = options.load
|
|
1510
|
+
.filter(({ cargoId }) => cargoId !== currentFleet.ammoBank.cargoId &&
|
|
1511
|
+
cargoId !== currentFleet.fuelTank.cargoId)
|
|
1512
|
+
.reduce((total, { amount, cargoId }) => total + amount * BigInt(definitions.get(cargoId).storageCost), 0n);
|
|
1513
|
+
requireTransferCapacity(`Fleet ${fleet.name} ammo destination`, fleet.address, currentFleet.ammoBank.amount, loadAmmo, currentFleet.stats.cargo.ammoCapacity);
|
|
1514
|
+
requireTransferCapacity(`Fleet ${fleet.name} fuel destination`, fleet.address, currentFleet.fuelTank.amount, loadFuel, currentFleet.stats.cargo.fuelCapacity);
|
|
1515
|
+
requireTransferCapacity(`Fleet ${fleet.name} cargo destination`, fleet.address, currentFleet.cargoHold.storageCost, loadCargoStorage, currentFleet.stats.cargo.cargoCapacity);
|
|
1516
|
+
const unloadStorage = options.unload.reduce((total, { amount, cargoId }) => total + amount * BigInt(definitions.get(cargoId).storageCost), 0n);
|
|
1517
|
+
requireTransferCapacity(`Claim Stake ${stake.address} resource destination`, stake.address, currentStake.production.cargoPod.storageCost, unloadStorage, currentStake.production.resourceCapacityRaw);
|
|
1518
|
+
const descriptions = [
|
|
1519
|
+
...describeCargoRequests('load', options.load),
|
|
1520
|
+
...describeCargoRequests('unload', options.unload),
|
|
1521
|
+
];
|
|
1522
|
+
const description = `Start fleet ${fleet.name}'s transfer to Claim Stake ${stake.address}: ${descriptions.join('; ')}. Range and transfer duration remain chain-authoritative.`;
|
|
1523
|
+
return createPlan({
|
|
1524
|
+
kind: 'claim-stakes.start-fleet-transfer',
|
|
1525
|
+
preconditions: await Promise.all([
|
|
1526
|
+
precondition(fleetAccount, `Fleet ${fleet.name} retains its owner, name, idle state, normalized crew, cargo balances, and destination capacities.`),
|
|
1527
|
+
precondition(stakeAccount, `Claim Stake ${stake.address} retains its owner, active state, cargo balances, destination capacity, Body, and System generation.`),
|
|
1528
|
+
precondition(bodyAccount, `The Claim Stake Planet remains assigned to System ${system} at the current generation.`),
|
|
1529
|
+
precondition(characterAccount, `The canonical Character remains assigned to Profile ${options.authorization.profile} in the configured Game.`),
|
|
1530
|
+
precondition(systemAccount, `Star System ${currentSystem.name} retains its configured Game, sequence, nested Starbase, and Claim Stake Body relationship.`),
|
|
1531
|
+
precondition(playerAccount, `Starbase Player ${player} retains its Profile, Character, and System.`),
|
|
1532
|
+
precondition(currencyAccount, 'The canonical CurrencyCache remains assigned to the configured Game.'),
|
|
1533
|
+
...(crewCandidate === undefined
|
|
1534
|
+
? []
|
|
1535
|
+
: [
|
|
1536
|
+
precondition(crewCandidate, `Fleet ${fleet.name}'s optional FleetCrewBinding remains assigned to the Fleet and owner Profile.`),
|
|
1537
|
+
]),
|
|
1538
|
+
]),
|
|
1539
|
+
steps: [
|
|
1540
|
+
{
|
|
1541
|
+
describes: description,
|
|
1542
|
+
instruction: adaptStartClaimStakeFleetTransferInstruction({
|
|
1543
|
+
authorization: options.authorization,
|
|
1544
|
+
celestialBody: stake.body.address,
|
|
1545
|
+
character,
|
|
1546
|
+
claimStakeInstance: stake.address,
|
|
1547
|
+
...(crewCandidate === undefined ? {} : { crewBinding }),
|
|
1548
|
+
currencyCache,
|
|
1549
|
+
fleet: fleet.address,
|
|
1550
|
+
game,
|
|
1551
|
+
systemAndStarbasePlayerStarbasePlayer: player,
|
|
1552
|
+
systemAndStarbasePlayerSystem: system,
|
|
1553
|
+
toLoad: options.load.map(({ amount, cargoId }) => [cargoId, amount]),
|
|
1554
|
+
toUnload: options.unload.map(({ amount, cargoId }) => [
|
|
1555
|
+
cargoId,
|
|
1556
|
+
amount,
|
|
1557
|
+
]),
|
|
1558
|
+
}),
|
|
1559
|
+
signers: [options.authorization.authority],
|
|
1560
|
+
},
|
|
1561
|
+
],
|
|
1562
|
+
summary: description,
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
/** Plans owner-authorized recovery of one provably non-executable Claim Stake transfer.
|
|
1566
|
+
*
|
|
1567
|
+
* Recovery is available only after the explicit transfer lock time and only
|
|
1568
|
+
* when fresh Fleet/Claim Stake cargo and Game definition state proves a source
|
|
1569
|
+
* shortfall, invalid queued cargo id, or destination overflow. An executable
|
|
1570
|
+
* transfer is rejected so callers use the permissionless normal exit instead.
|
|
1571
|
+
* Recovery returns the Fleet to idle without moving cargo or changing the
|
|
1572
|
+
* Claim Stake.
|
|
1573
|
+
*
|
|
1574
|
+
* @example
|
|
1575
|
+
* ```ts
|
|
1576
|
+
* declare const ctx: SageContext;
|
|
1577
|
+
* declare const fleet: FleetSnapshot;
|
|
1578
|
+
* declare const stake: ClaimStakeSnapshot;
|
|
1579
|
+
* declare const authorization: PlanAuthorization;
|
|
1580
|
+
* const plan = await planRecoverClaimStakeFleetTransfer(ctx, fleet, stake, {
|
|
1581
|
+
* atUnixSeconds: 1_700_000_100n,
|
|
1582
|
+
* authorization,
|
|
1583
|
+
* });
|
|
1584
|
+
* console.log(plan.describe());
|
|
1585
|
+
* ```
|
|
1586
|
+
*/
|
|
1587
|
+
export async function planRecoverClaimStakeFleetTransfer(ctx, fleet, stake, input) {
|
|
1588
|
+
const options = validateRecoverFleetTransferOptions(input);
|
|
1589
|
+
const game = requireGameAddress(ctx);
|
|
1590
|
+
if (fleet.game !== game || stake.game !== game) {
|
|
1591
|
+
fail(`Fleet ${fleet.address} and Claim Stake ${stake.address} must both belong to configured Game ${game}. Refresh both snapshots through this context and retry recovery.`, fleet.game !== game ? fleet.address : stake.address);
|
|
1592
|
+
}
|
|
1593
|
+
if (fleet.ownerProfile.address !== options.authorization.profile) {
|
|
1594
|
+
fail(`Fleet ${fleet.name} belongs to Profile ${fleet.ownerProfile.address}, not authorization Profile ${options.authorization.profile}. Use the owning Profile, refresh the Fleet, and retry recovery.`, fleet.address);
|
|
1595
|
+
}
|
|
1596
|
+
if (fleet.state.kind !== 'claimStakeTransfer') {
|
|
1597
|
+
fail(`Fleet ${fleet.name} is ${fleet.state.kind}, not in a Claim Stake transfer. Refresh it and recover only a stuck Claim Stake transfer.`, fleet.address);
|
|
1598
|
+
}
|
|
1599
|
+
if (fleet.state.claimStake.address !== stake.address ||
|
|
1600
|
+
fleet.state.planet.address !== stake.body.address) {
|
|
1601
|
+
fail(`Fleet ${fleet.name}'s Claim Stake transfer does not reference Claim Stake ${stake.address} and its Body ${stake.body.address}. Supply the transfer's current Claim Stake snapshot and retry recovery.`, fleet.address);
|
|
1602
|
+
}
|
|
1603
|
+
if (stake.state.kind !== 'active') {
|
|
1604
|
+
fail(`Claim Stake ${stake.address} is ${stake.state.kind}, but recovery requires the referenced Claim Stake to remain active. Refresh it and use force-exit only for a program-closed stake.`, stake.address);
|
|
1605
|
+
}
|
|
1606
|
+
if (options.atUnixSeconds < fleet.state.endAtUnixSeconds) {
|
|
1607
|
+
fail(`Fleet ${fleet.name}'s Claim Stake transfer lock has not elapsed at Unix timestamp ${String(options.atUnixSeconds)}; it ends at ${String(fleet.state.endAtUnixSeconds)}. Wait until the lock elapses, provide a current timestamp, and retry recovery.`, fleet.address);
|
|
1608
|
+
}
|
|
1609
|
+
const character = await findCharacterPda(options.authorization.profile, game);
|
|
1610
|
+
if (stake.ownerCharacter.address !== character) {
|
|
1611
|
+
fail(`Claim Stake ${stake.address} does not belong to authorization Profile ${options.authorization.profile}'s canonical Character ${character}. Supply the owned current Claim Stake and retry recovery.`, stake.address);
|
|
1612
|
+
}
|
|
1613
|
+
const [fleetCandidate, stakeCandidate] = await readContextRawAccounts(ctx, [
|
|
1614
|
+
fleet.address,
|
|
1615
|
+
stake.address,
|
|
1616
|
+
]);
|
|
1617
|
+
const fleetAccount = requireAccount(fleetCandidate, fleet.address, `Fleet ${fleet.name} is missing. Refresh it and retry Claim Stake transfer recovery.`);
|
|
1618
|
+
const stakeAccount = requireAccount(stakeCandidate, stake.address, `Claim Stake ${stake.address} is missing. Refresh it and retry Fleet transfer recovery.`);
|
|
1619
|
+
const currentFleet = fleetAccountDefinition.decode(fleetAccount);
|
|
1620
|
+
const currentStake = claimStakeAccountDefinition.decode(stakeAccount);
|
|
1621
|
+
if (currentFleet.game !== game ||
|
|
1622
|
+
currentFleet.ownerProfile !== options.authorization.profile ||
|
|
1623
|
+
currentFleet.name !== fleet.name ||
|
|
1624
|
+
currentFleet.state.kind !== 'claimStakeTransfer' ||
|
|
1625
|
+
currentFleet.state.claimStake !== stake.address ||
|
|
1626
|
+
currentFleet.state.planet !== stake.body.address ||
|
|
1627
|
+
currentFleet.state.endAtUnixSeconds !== fleet.state.endAtUnixSeconds ||
|
|
1628
|
+
!sameCargoAmounts(currentFleet.state.load, fleet.state.load) ||
|
|
1629
|
+
!sameCargoAmounts(currentFleet.state.unload, fleet.state.unload) ||
|
|
1630
|
+
(await deriveFleetAddress(currentFleet)) !== fleet.address ||
|
|
1631
|
+
currentFleet.ammoBank.cargoId !== fleet.ammo.id ||
|
|
1632
|
+
currentFleet.ammoBank.amount !== fleet.ammo.amount ||
|
|
1633
|
+
currentFleet.fuelTank.cargoId !== fleet.fuel.id ||
|
|
1634
|
+
currentFleet.fuelTank.amount !== fleet.fuel.amount ||
|
|
1635
|
+
currentFleet.cargoHold.storageCost !== fleet.cargoHold.storageCost ||
|
|
1636
|
+
!sameCargoAmounts(currentFleet.cargoHold.amounts, fleet.cargoHold.items) ||
|
|
1637
|
+
currentFleet.stats.cargo.ammoCapacity !== fleet.capacities.ammo.total ||
|
|
1638
|
+
currentFleet.stats.cargo.fuelCapacity !== fleet.capacities.fuel.total ||
|
|
1639
|
+
currentFleet.stats.cargo.cargoCapacity !== fleet.capacities.cargo.total) {
|
|
1640
|
+
fail(`Fleet ${fleet.name} changed or no longer matches its loaded owner, Game, canonical address, Claim Stake transfer snapshot, cargo, or capacities. Refresh it and retry recovery.`, fleet.address);
|
|
1641
|
+
}
|
|
1642
|
+
if (currentStake.game !== game ||
|
|
1643
|
+
currentStake.ownerCharacter !== character ||
|
|
1644
|
+
currentStake.body !== stake.body.address ||
|
|
1645
|
+
currentStake.systemSeqId !== stake.systemSeqId ||
|
|
1646
|
+
currentStake.state.kind !== 'active' ||
|
|
1647
|
+
currentStake.production.cargoPod.storageCost !==
|
|
1648
|
+
stake.resources.capacity.usedRaw ||
|
|
1649
|
+
currentStake.production.resourceCapacityRaw !==
|
|
1650
|
+
stake.resources.capacity.totalRaw ||
|
|
1651
|
+
!sameCargoAmounts(currentStake.production.cargoPod.amounts, stake.resources.inventory.items.map(({ id, quantityRaw }) => ({
|
|
1652
|
+
amount: quantityRaw,
|
|
1653
|
+
id,
|
|
1654
|
+
})))) {
|
|
1655
|
+
fail(`Claim Stake ${stake.address} changed or no longer matches its loaded owner, Game, Body, active state, resources, capacity, or System generation. Refresh it and retry Fleet transfer recovery.`, stake.address);
|
|
1656
|
+
}
|
|
1657
|
+
let reason;
|
|
1658
|
+
for (const request of currentFleet.state.load) {
|
|
1659
|
+
const available = cargoAmount(currentStake.production.cargoPod.amounts, request.cargoId);
|
|
1660
|
+
if (available < request.amount) {
|
|
1661
|
+
reason = `Claim Stake source shortfall for cargo id ${String(request.cargoId)}`;
|
|
1662
|
+
break;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
if (reason === undefined) {
|
|
1666
|
+
for (const request of currentFleet.state.unload) {
|
|
1667
|
+
const available = fleetCargoAmount(currentFleet, request.cargoId);
|
|
1668
|
+
if (available < request.amount) {
|
|
1669
|
+
reason = `Fleet source shortfall for cargo id ${String(request.cargoId)}`;
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
let gameAccount;
|
|
1675
|
+
let queuedDefinitions = new Map();
|
|
1676
|
+
if (reason === undefined) {
|
|
1677
|
+
const cargoIds = [
|
|
1678
|
+
...new Set([...currentFleet.state.load, ...currentFleet.state.unload].map(({ cargoId }) => cargoId)),
|
|
1679
|
+
];
|
|
1680
|
+
const freshGame = (await readFreshGameCargoDefinitions(ctx)) ??
|
|
1681
|
+
fail(`Game ${game} is missing. Refresh the configured Game and retry Claim Stake transfer recovery.`, game);
|
|
1682
|
+
gameAccount = freshGame.account;
|
|
1683
|
+
queuedDefinitions = new Map(freshGame.data.definitions.map((definition) => [
|
|
1684
|
+
definition.id,
|
|
1685
|
+
definition,
|
|
1686
|
+
]));
|
|
1687
|
+
const invalidCargoId = cargoIds.find((cargoId) => !queuedDefinitions.has(cargoId));
|
|
1688
|
+
if (invalidCargoId !== undefined) {
|
|
1689
|
+
reason = `invalid queued cargo id ${String(invalidCargoId)}`;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (reason === undefined) {
|
|
1693
|
+
const loadAmmo = cargoAmount(currentFleet.state.load, currentFleet.ammoBank.cargoId);
|
|
1694
|
+
const loadFuel = cargoAmount(currentFleet.state.load, currentFleet.fuelTank.cargoId);
|
|
1695
|
+
const loadCargoStorage = currentFleet.state.load
|
|
1696
|
+
.filter(({ cargoId }) => cargoId !== currentFleet.ammoBank.cargoId &&
|
|
1697
|
+
cargoId !== currentFleet.fuelTank.cargoId)
|
|
1698
|
+
.reduce((total, { amount, cargoId }) => total + amount * BigInt(queuedDefinitions.get(cargoId).storageCost), 0n);
|
|
1699
|
+
const unloadStorage = currentFleet.state.unload.reduce((total, { amount, cargoId }) => total + amount * BigInt(queuedDefinitions.get(cargoId).storageCost), 0n);
|
|
1700
|
+
if (currentFleet.ammoBank.amount + loadAmmo >
|
|
1701
|
+
currentFleet.stats.cargo.ammoCapacity) {
|
|
1702
|
+
reason = 'Fleet ammo destination overflow';
|
|
1703
|
+
}
|
|
1704
|
+
else if (currentFleet.fuelTank.amount + loadFuel >
|
|
1705
|
+
currentFleet.stats.cargo.fuelCapacity) {
|
|
1706
|
+
reason = 'Fleet fuel destination overflow';
|
|
1707
|
+
}
|
|
1708
|
+
else if (currentFleet.cargoHold.storageCost + loadCargoStorage >
|
|
1709
|
+
currentFleet.stats.cargo.cargoCapacity) {
|
|
1710
|
+
reason = 'Fleet cargo destination overflow';
|
|
1711
|
+
}
|
|
1712
|
+
else if (currentStake.production.cargoPod.storageCost + unloadStorage >
|
|
1713
|
+
currentStake.production.resourceCapacityRaw) {
|
|
1714
|
+
reason = 'Claim Stake resource destination overflow';
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (reason === undefined) {
|
|
1718
|
+
fail(`Fleet ${fleet.name}'s Claim Stake transfer is currently executable. Use the permissionless normal exit path; recovery is only for a locally proven non-executable transfer.`, fleet.address);
|
|
1719
|
+
}
|
|
1720
|
+
const description = `Recover fleet ${fleet.name} from its stuck Claim Stake transfer (${reason}); no cargo moves and the Fleet returns to idle.`;
|
|
1721
|
+
return createPlan({
|
|
1722
|
+
kind: 'claim-stakes.recover-fleet-transfer',
|
|
1723
|
+
preconditions: await Promise.all([
|
|
1724
|
+
precondition(fleetAccount, `Fleet ${fleet.name} retains its owner and exact elapsed, non-executable Claim Stake transfer snapshot.`),
|
|
1725
|
+
precondition(stakeAccount, `Claim Stake ${stake.address} remains active with the current owner, cargo, and capacity used to prove non-executability.`),
|
|
1726
|
+
...(gameAccount === undefined
|
|
1727
|
+
? []
|
|
1728
|
+
: [
|
|
1729
|
+
precondition(gameAccount, `Game ${game} retains the cargo definitions used to prove the transfer is non-executable.`),
|
|
1730
|
+
]),
|
|
1731
|
+
]),
|
|
1732
|
+
steps: [
|
|
1733
|
+
{
|
|
1734
|
+
describes: description,
|
|
1735
|
+
instruction: adaptRecoverClaimStakeFleetTransferInstruction({
|
|
1736
|
+
authorization: options.authorization,
|
|
1737
|
+
claimStakeInstance: stake.address,
|
|
1738
|
+
fleet: fleet.address,
|
|
1739
|
+
game,
|
|
1740
|
+
}),
|
|
1741
|
+
signers: [options.authorization.authority],
|
|
1742
|
+
},
|
|
1743
|
+
],
|
|
1744
|
+
summary: description,
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
//# sourceMappingURL=index.js.map
|