payment-kit 1.25.0 → 1.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@ import logger from '../../libs/logger';
13
13
  import { Customer, GroupedBN, PaymentCurrency, PaymentMethod, Subscription } from '../../store/models';
14
14
  import { fetchErc20Balance, fetchEtherBalance } from '../ethereum/token';
15
15
  import { EVM_CHAIN_TYPES } from '../../libs/constants';
16
+ import { getAppStakeAddressWithFallback, getMigratedFromList } from '../../libs/wallet-migration';
16
17
 
17
18
  export async function ensureStakedForGas() {
18
19
  const currencies = await PaymentCurrency.findAll({ where: { active: true, is_base_currency: true } });
@@ -26,10 +27,10 @@ export async function ensureStakedForGas() {
26
27
  try {
27
28
  const { state: account } = await client.getAccountState({ address: wallet.address });
28
29
 
29
- const address = toStakeAddress(wallet.address, wallet.address);
30
- const { state: stake } = await client.getStakeState({ address });
31
- if (stake) {
32
- logger.info(`app already staked for gas on chain ${host}`);
30
+ // Use migration-aware fallback to check if app has staked for gas
31
+ const stakeResult = await getAppStakeAddressWithFallback({ client });
32
+ if (stakeResult) {
33
+ logger.info(`app already staked for gas on chain ${host}`, { source: stakeResult.source });
33
34
  continue;
34
35
  }
35
36
 
@@ -59,6 +60,14 @@ export async function ensureStakedForGas() {
59
60
  export async function hasStakedForGas(method: PaymentMethod, sender = wallet.address) {
60
61
  if (method.type === 'arcblock') {
61
62
  const client = method.getOcapClient();
63
+
64
+ // If checking app's own stake, use migration-aware fallback
65
+ if (sender === wallet.address) {
66
+ const stakeResult = await getAppStakeAddressWithFallback({ client });
67
+ return !!stakeResult;
68
+ }
69
+
70
+ // For other senders, use direct query
62
71
  const address = toStakeAddress(sender, sender);
63
72
  const { state } = await client.getStakeState({ address });
64
73
  return !!state;
@@ -195,23 +204,32 @@ export async function getStakeSummaryByDid(did: string, livemode: boolean = true
195
204
  await Promise.all(
196
205
  methods.map(async (method: PaymentMethod) => {
197
206
  const client = method.getOcapClient();
198
- const { stakes } = await client.listStakes({
199
- addressFilter: {
200
- receiver: wallet.address,
201
- },
202
- });
203
- (stakes || [])
204
- .filter((x: any) => x.sender === did)
205
- .forEach((x: any) => {
206
- const { tokens } = x;
207
- (tokens || []).forEach((t: any) => {
208
- // @ts-ignore
209
- const currency = method.payment_currencies?.find((c: PaymentCurrency) => t.address === c.contract);
210
- if (currency) {
211
- results[currency.id] = new BN(results[currency.id] || '0').add(new BN(t.balance || '0')).toString();
212
- }
213
- });
207
+
208
+ // Get all possible receiver addresses (current + migratedFrom) for migration compatibility
209
+ const migratedFrom = await getMigratedFromList(client);
210
+ const receiverAddresses = [...new Set([wallet.address, ...migratedFrom])];
211
+
212
+ // Query stakes for all possible receiver addresses
213
+ for (const receiver of receiverAddresses) {
214
+ // eslint-disable-next-line no-await-in-loop
215
+ const { stakes } = await client.listStakes({
216
+ addressFilter: {
217
+ receiver,
218
+ },
214
219
  });
220
+ (stakes || [])
221
+ .filter((x: any) => x.sender === did)
222
+ .forEach((x: any) => {
223
+ const { tokens } = x;
224
+ (tokens || []).forEach((t: any) => {
225
+ // @ts-ignore
226
+ const currency = method.payment_currencies?.find((c: PaymentCurrency) => t.address === c.contract);
227
+ if (currency) {
228
+ results[currency.id] = new BN(results[currency.id] || '0').add(new BN(t.balance || '0')).toString();
229
+ }
230
+ });
231
+ });
232
+ }
215
233
  })
216
234
  );
217
235
 
@@ -1,6 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/indent */
2
2
  import { isEthereumDid } from '@arcblock/did';
3
- import { toDelegateAddress } from '@arcblock/did-util';
4
3
  import { getWalletDid } from '@blocklet/sdk/lib/did';
5
4
  import type { DelegateState, TokenLimit } from '@ocap/client';
6
5
  import { toTxHash } from '@ocap/mcrypto';
@@ -20,7 +19,9 @@ import {
20
19
  Payout,
21
20
  CreditGrant,
22
21
  Customer,
22
+ Subscription,
23
23
  } from '../store/models';
24
+ import { getDelegationAddressWithFallback, backfillDelegationAddress, getMigratedFromList } from './wallet-migration';
24
25
  import type { TPaymentCurrency } from '../store/models/payment-currency';
25
26
  import { blocklet, ethWallet, wallet, getVaultAddress } from './auth';
26
27
  import logger from './logger';
@@ -212,8 +213,11 @@ export async function isDelegationSufficientForPayment(args: {
212
213
  amount: string;
213
214
  delegatorAmounts?: string[];
214
215
  lineItems?: TLineItemExpanded[];
216
+ subscription?: Subscription | null; // Optional subscription for migration fallback
217
+ storedDelegationAddress?: string; // Direct stored address (for auto-recharge etc.)
215
218
  }): Promise<SufficientForPaymentResult> {
216
- const { paymentCurrency, paymentMethod, userDid, amount, delegatorAmounts } = args;
219
+ const { paymentCurrency, paymentMethod, userDid, amount, delegatorAmounts, subscription, storedDelegationAddress } =
220
+ args;
217
221
  if (!userDid) {
218
222
  return { sufficient: false, reason: 'NO_DID_WALLET' };
219
223
  }
@@ -251,14 +255,32 @@ export async function isDelegationSufficientForPayment(args: {
251
255
 
252
256
  const client = paymentMethod.getOcapClient();
253
257
 
254
- // have delegated before?
255
- const address = toDelegateAddress(delegator, wallet.address);
258
+ // have delegated before? Use migration-aware fallback query
259
+ // Priority: storedDelegationAddress > subscription.payment_details.arcblock.delegation_address
260
+ const delegationResult = await getDelegationAddressWithFallback({
261
+ storedAddress: storedDelegationAddress || subscription?.payment_details?.arcblock?.delegation_address,
262
+ delegator,
263
+ client,
264
+ });
265
+ if (!delegationResult) {
266
+ return { sufficient: false, reason: 'NO_DELEGATION' };
267
+ }
268
+
269
+ const { address, needsBackfill, source } = delegationResult;
270
+
271
+ // Backfill if needed and subscription is available
272
+ if (needsBackfill && subscription) {
273
+ await backfillDelegationAddress(subscription.id, address);
274
+ }
275
+
256
276
  const { state } = await client.getDelegateState({ address });
257
277
  if (!state) {
278
+ logger.error('isDelegationSufficientForPayment: no delegation state', { address, delegator, source });
258
279
  return { sufficient: false, reason: 'NO_DELEGATION' };
259
280
  }
260
281
 
261
282
  if (!state.ops || state.ops?.length === 0) {
283
+ logger.error('isDelegationSufficientForPayment: no delegation ops', { address, delegator, source });
262
284
  return { sufficient: false, reason: 'NO_DELEGATION' };
263
285
  }
264
286
 
@@ -277,8 +299,14 @@ export async function isDelegationSufficientForPayment(args: {
277
299
 
278
300
  // FIXME: @wangshijun check other conditions in the token limit: txCount, totalAllowance, validUntil, rateLimit
279
301
 
280
- if (Array.isArray(tokenLimit.to) && tokenLimit.to.length && tokenLimit.to.includes(wallet.address) === false) {
281
- return { sufficient: false, reason: 'NO_TRANSFER_TO' };
302
+ // Check if tokenLimit.to includes current wallet.address or any migratedFrom addresses
303
+ if (Array.isArray(tokenLimit.to) && tokenLimit.to.length) {
304
+ const migratedFrom = await getMigratedFromList(client);
305
+ const allWalletAddresses = [wallet.address, ...migratedFrom];
306
+ const hasValidTo = allWalletAddresses.some((addr) => tokenLimit.to.includes(addr));
307
+ if (!hasValidTo) {
308
+ return { sufficient: false, reason: 'NO_TRANSFER_TO' };
309
+ }
282
310
  }
283
311
 
284
312
  // check allowance
@@ -35,6 +35,7 @@ import type {
35
35
  } from '../store/models/types';
36
36
  import { wallet } from './auth';
37
37
  import logger from './logger';
38
+ import { getMigratedFromList } from './wallet-migration';
38
39
  import { applyDiscountsToLineItems } from './discount/discount';
39
40
  import { getQuoteService, QuoteResponse } from './quote-service';
40
41
  import { getExchangeRateService } from './exchange-rate';
@@ -443,8 +444,23 @@ export function getMinStakeAmount(config: Record<string, any> = {}) {
443
444
  return 0;
444
445
  }
445
446
 
446
- export function canPayWithDelegation(beneficiaries: PaymentBeneficiary[]) {
447
- return beneficiaries.length === 0 || beneficiaries.every((x) => x.address === wallet.address);
447
+ export async function canPayWithDelegation(
448
+ beneficiaries: PaymentBeneficiary[],
449
+ paymentMethod?: PaymentMethod
450
+ ): Promise<boolean> {
451
+ if (beneficiaries.length === 0) {
452
+ return true;
453
+ }
454
+
455
+ // Get all valid wallet addresses (current + migrated)
456
+ let allWalletAddresses = [wallet.address];
457
+ if (paymentMethod?.type === 'arcblock') {
458
+ const client = paymentMethod.getOcapClient();
459
+ const migratedFrom = await getMigratedFromList(client);
460
+ allWalletAddresses = [wallet.address, ...migratedFrom];
461
+ }
462
+
463
+ return beneficiaries.every((x) => allWalletAddresses.includes(x.address));
448
464
  }
449
465
 
450
466
  export function createPaymentBeneficiaries(total: string, beneficiaries: PaymentBeneficiary[]) {
@@ -37,6 +37,7 @@ import { trimDecimals, limitTokenPrecision } from './math-utils';
37
37
  import { getPriceCurrencyOptions, getPriceUintAmountByCurrency } from './price';
38
38
  import { getRecurringPeriod, getSubscriptionCreateSetup, SlippageOptions } from './session';
39
39
  import { getConnectQueryParam, getCustomerStakeAddress } from './util';
40
+ import { getStakingAddressWithFallback } from './wallet-migration';
40
41
  import { wallet } from './auth';
41
42
  import { getGasPayerExtra } from './payment';
42
43
  import { getLock } from './lock';
@@ -1056,11 +1057,32 @@ export function getSubscriptionTrialSetup(data: Partial<SubscriptionData>, curre
1056
1057
  };
1057
1058
  }
1058
1059
 
1059
- export async function getSubscriptionStakeAddress(subscription: Subscription, customerDid: string) {
1060
- return (
1061
- subscription.payment_details?.arcblock?.staking?.address ||
1062
- (await getCustomerStakeAddress(customerDid, subscription.id))
1063
- );
1060
+ export async function getSubscriptionStakeAddress(
1061
+ subscription: Subscription,
1062
+ customerDid: string,
1063
+ paymentMethod?: PaymentMethod
1064
+ ) {
1065
+ // If stored address exists, use it
1066
+ const storedAddress = subscription.payment_details?.arcblock?.staking?.address;
1067
+ if (storedAddress) {
1068
+ return storedAddress;
1069
+ }
1070
+
1071
+ // Use migration-aware fallback if paymentMethod is available
1072
+ if (paymentMethod?.type === 'arcblock') {
1073
+ const client = paymentMethod.getOcapClient();
1074
+ const stakingResult = await getStakingAddressWithFallback({
1075
+ userDid: customerDid,
1076
+ nonce: subscription.id,
1077
+ client,
1078
+ });
1079
+ if (stakingResult) {
1080
+ return stakingResult.address;
1081
+ }
1082
+ }
1083
+
1084
+ // Fallback to direct calculation
1085
+ return getCustomerStakeAddress(customerDid, subscription.id);
1064
1086
  }
1065
1087
 
1066
1088
  export async function getSubscriptionStakeCancellation(
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Wallet Migration Compatibility
3
+ *
4
+ * Provides utilities to handle wallet address changes after app migration.
5
+ * When an app's DID changes, the derived addresses (delegation, staking) also change,
6
+ * but user authorizations were made with the old addresses.
7
+ *
8
+ * This module provides fallback query mechanisms using migratedFrom list.
9
+ */
10
+
11
+ import { toDelegateAddress, toStakeAddress } from '@arcblock/did-util';
12
+ import type OcapClient from '@ocap/client';
13
+
14
+ import { wallet } from './auth';
15
+ import logger from './logger';
16
+ import { Subscription } from '../store/models';
17
+
18
+ // Cache for migratedFrom list (refreshed when wallet.address changes)
19
+ let cachedMigratedFrom: string[] | null = null;
20
+ let cachedWalletAddress: string | null = null;
21
+
22
+ /**
23
+ * Get the migratedFrom list for the current app wallet (with caching)
24
+ * The cache is invalidated when wallet.address changes
25
+ */
26
+ export async function getMigratedFromList(client: OcapClient): Promise<string[]> {
27
+ // If wallet address hasn't changed, use cached value
28
+ if (wallet.address === cachedWalletAddress && cachedMigratedFrom !== null) {
29
+ return cachedMigratedFrom;
30
+ }
31
+
32
+ // Query chain for migratedFrom list
33
+ try {
34
+ const { state } = await client.getAccountState({ address: wallet.address });
35
+ cachedMigratedFrom = state?.migratedFrom || [];
36
+ cachedWalletAddress = wallet.address;
37
+
38
+ if (cachedMigratedFrom.length > 0) {
39
+ logger.info('wallet-migration: loaded migratedFrom list', {
40
+ walletAddress: wallet.address,
41
+ migratedFrom: cachedMigratedFrom,
42
+ });
43
+ }
44
+
45
+ return cachedMigratedFrom;
46
+ } catch (err) {
47
+ logger.error('wallet-migration: failed to get migratedFrom list', { error: err });
48
+ return [];
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Clear the migratedFrom cache (for testing purposes)
54
+ */
55
+ export function clearMigratedFromCache(): void {
56
+ cachedMigratedFrom = null;
57
+ cachedWalletAddress = null;
58
+ }
59
+
60
+ export type AddressWithFallbackResult = {
61
+ address: string;
62
+ needsBackfill: boolean;
63
+ source: 'stored' | 'current' | 'migrated';
64
+ };
65
+
66
+ export type GetDelegationAddressParams = {
67
+ /** Stored delegation address from payment_details (if available) */
68
+ storedAddress?: string;
69
+ /** The delegator's DID (user's wallet DID) */
70
+ delegator: string;
71
+ /** OCAP client for chain queries */
72
+ client: OcapClient;
73
+ };
74
+
75
+ /**
76
+ * Get the effective delegation address with migration fallback
77
+ *
78
+ * Priority:
79
+ * 1. Use stored delegation_address if provided
80
+ * 2. Try current wallet.address
81
+ * 3. Fallback to migratedFrom addresses
82
+ */
83
+ export async function getDelegationAddressWithFallback({
84
+ storedAddress,
85
+ delegator,
86
+ client,
87
+ }: GetDelegationAddressParams): Promise<AddressWithFallbackResult | null> {
88
+ // 1. Check stored delegation_address first
89
+ if (storedAddress) {
90
+ return { address: storedAddress, needsBackfill: false, source: 'stored' };
91
+ }
92
+
93
+ // 2. Try current wallet.address
94
+ const currentAddress = toDelegateAddress(delegator, wallet.address);
95
+ try {
96
+ const { state: currentState } = await client.getDelegateState({ address: currentAddress });
97
+ if (currentState?.ops?.length > 0) {
98
+ return { address: currentAddress, needsBackfill: true, source: 'current' };
99
+ }
100
+ } catch (err) {
101
+ logger.warn('wallet-migration: failed to query current delegation state', {
102
+ address: currentAddress,
103
+ error: err,
104
+ });
105
+ }
106
+
107
+ // 3. Fallback to migratedFrom addresses
108
+ const migratedFrom = await getMigratedFromList(client);
109
+ for (const oldAppDid of migratedFrom) {
110
+ const oldAddress = toDelegateAddress(delegator, oldAppDid);
111
+ try {
112
+ // eslint-disable-next-line no-await-in-loop
113
+ const { state: oldState } = await client.getDelegateState({ address: oldAddress });
114
+ if (oldState?.ops?.length > 0) {
115
+ logger.info('wallet-migration: found delegation in migratedFrom', {
116
+ delegator,
117
+ oldAppDid,
118
+ oldAddress,
119
+ });
120
+ return { address: oldAddress, needsBackfill: true, source: 'migrated' };
121
+ }
122
+ } catch (err) {
123
+ logger.warn('wallet-migration: failed to query migrated delegation state', {
124
+ address: oldAddress,
125
+ oldAppDid,
126
+ error: err,
127
+ });
128
+ }
129
+ }
130
+
131
+ // Not found
132
+ return null;
133
+ }
134
+
135
+ export type GetStakingAddressParams = {
136
+ /** Stored staking address from payment_details (if available) */
137
+ storedAddress?: string;
138
+ /** The user's DID */
139
+ userDid: string;
140
+ /** The nonce for stake address derivation (usually subscription ID) */
141
+ nonce: string;
142
+ /** OCAP client for chain queries */
143
+ client: OcapClient;
144
+ };
145
+
146
+ /**
147
+ * Get the effective staking address with migration fallback
148
+ *
149
+ * Priority:
150
+ * 1. Use stored staking.address if provided
151
+ * 2. Try current wallet.address
152
+ * 3. Fallback to migratedFrom addresses
153
+ */
154
+ export async function getStakingAddressWithFallback({
155
+ storedAddress,
156
+ userDid,
157
+ nonce,
158
+ client,
159
+ }: GetStakingAddressParams): Promise<AddressWithFallbackResult | null> {
160
+ // 1. Check stored staking.address first
161
+ if (storedAddress) {
162
+ return { address: storedAddress, needsBackfill: false, source: 'stored' };
163
+ }
164
+
165
+ // 2. Try current wallet.address
166
+ const currentAddress = toStakeAddress(userDid, wallet.address, nonce);
167
+ try {
168
+ const { state: currentState } = await client.getStakeState({ address: currentAddress });
169
+ if (currentState) {
170
+ return { address: currentAddress, needsBackfill: true, source: 'current' };
171
+ }
172
+ } catch (err) {
173
+ logger.warn('wallet-migration: failed to query current stake state', {
174
+ address: currentAddress,
175
+ error: err,
176
+ });
177
+ }
178
+
179
+ // 3. Fallback to migratedFrom addresses
180
+ const migratedFrom = await getMigratedFromList(client);
181
+ for (const oldAppDid of migratedFrom) {
182
+ const oldAddress = toStakeAddress(userDid, oldAppDid, nonce);
183
+ try {
184
+ // eslint-disable-next-line no-await-in-loop
185
+ const { state: oldState } = await client.getStakeState({ address: oldAddress });
186
+ if (oldState) {
187
+ logger.info('wallet-migration: found stake in migratedFrom', {
188
+ userDid,
189
+ nonce,
190
+ oldAppDid,
191
+ oldAddress,
192
+ });
193
+ return { address: oldAddress, needsBackfill: true, source: 'migrated' };
194
+ }
195
+ } catch (err) {
196
+ logger.warn('wallet-migration: failed to query migrated stake state', {
197
+ address: oldAddress,
198
+ oldAppDid,
199
+ error: err,
200
+ });
201
+ }
202
+ }
203
+
204
+ // Not found
205
+ return null;
206
+ }
207
+
208
+ export type GetAppStakeAddressParams = {
209
+ /** OCAP client for chain queries */
210
+ client: OcapClient;
211
+ };
212
+
213
+ /**
214
+ * Get the effective staking address for gas with migration fallback
215
+ * This is for app's own stake (sender === receiver === wallet.address)
216
+ */
217
+ export async function getAppStakeAddressWithFallback({
218
+ client,
219
+ }: GetAppStakeAddressParams): Promise<AddressWithFallbackResult | null> {
220
+ // 1. Try current wallet.address
221
+ const currentAddress = toStakeAddress(wallet.address, wallet.address);
222
+ try {
223
+ const { state: currentState } = await client.getStakeState({ address: currentAddress });
224
+ if (currentState) {
225
+ return { address: currentAddress, needsBackfill: false, source: 'current' };
226
+ }
227
+ } catch (err) {
228
+ logger.warn('wallet-migration: failed to query current app stake state', {
229
+ address: currentAddress,
230
+ error: err,
231
+ });
232
+ }
233
+
234
+ // 2. Fallback to migratedFrom addresses
235
+ const migratedFrom = await getMigratedFromList(client);
236
+ for (const oldAppDid of migratedFrom) {
237
+ const oldAddress = toStakeAddress(oldAppDid, oldAppDid);
238
+ try {
239
+ // eslint-disable-next-line no-await-in-loop
240
+ const { state: oldState } = await client.getStakeState({ address: oldAddress });
241
+ if (oldState) {
242
+ logger.info('wallet-migration: found app stake in migratedFrom', {
243
+ oldAppDid,
244
+ oldAddress,
245
+ });
246
+ return { address: oldAddress, needsBackfill: false, source: 'migrated' };
247
+ }
248
+ } catch (err) {
249
+ logger.warn('wallet-migration: failed to query migrated app stake state', {
250
+ address: oldAddress,
251
+ oldAppDid,
252
+ error: err,
253
+ });
254
+ }
255
+ }
256
+
257
+ // Not found
258
+ return null;
259
+ }
260
+
261
+ /**
262
+ * Backfill delegation_address to subscription's payment_details (with optimistic locking)
263
+ *
264
+ * @param subscription - The subscription to update
265
+ * @param address - The delegation address to store
266
+ * @returns true if backfilled, false if already had a value
267
+ */
268
+ export async function backfillDelegationAddress(subId: string, address: string): Promise<boolean> {
269
+ // Optimistic lock: check if already has a value
270
+ // Use fresh read to avoid race conditions
271
+ if (!subId) {
272
+ return false;
273
+ }
274
+ try {
275
+ const subscription = await Subscription.findByPk(subId);
276
+ if (!subscription) {
277
+ return false;
278
+ }
279
+ if (subscription.payment_details?.arcblock?.delegation_address) {
280
+ // Already backfilled by another request
281
+ return false;
282
+ }
283
+
284
+ // Backfill the address (only if arcblock payment_details exists with required fields)
285
+ const existingArcblock = subscription.payment_details?.arcblock;
286
+ if (!existingArcblock?.tx_hash || !existingArcblock?.payer) {
287
+ logger.warn('wallet-migration: cannot backfill delegation_address - missing arcblock payment_details', {
288
+ subscriptionId: subscription.id,
289
+ });
290
+ return false;
291
+ }
292
+
293
+ await subscription.update({
294
+ payment_details: {
295
+ ...subscription.payment_details,
296
+ arcblock: {
297
+ ...existingArcblock,
298
+ delegation_address: address,
299
+ },
300
+ },
301
+ });
302
+
303
+ logger.info('wallet-migration: backfilled delegation_address', {
304
+ subscriptionId: subscription.id,
305
+ address,
306
+ });
307
+
308
+ return true;
309
+ } catch (err) {
310
+ logger.error('wallet-migration: failed to backfill delegation_address', {
311
+ subscriptionId: subId,
312
+ address,
313
+ error: err,
314
+ });
315
+ return false;
316
+ }
317
+ }
@@ -528,6 +528,7 @@ async function executeAutoRecharge(
528
528
  paymentCurrency: rechargeCurrency,
529
529
  userDid: payer,
530
530
  amount: totalAmount,
531
+ storedDelegationAddress: config.payment_details?.arcblock?.delegation_address,
531
532
  });
532
533
 
533
534
  if (!balanceCheck.sufficient) {
@@ -725,7 +725,7 @@ const handleStakeSlash = async (
725
725
  return;
726
726
  }
727
727
 
728
- const address = await getSubscriptionStakeAddress(subscription, customer.did);
728
+ const address = await getSubscriptionStakeAddress(subscription, customer.did, paymentMethod);
729
729
  const slashAmount = paymentIntent.amount;
730
730
  const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, slashAmount);
731
731
  if (!stakeEnough.enough) {
@@ -859,6 +859,8 @@ export const handlePayment = async (job: PaymentJob) => {
859
859
  }
860
860
 
861
861
  const invoice = await Invoice.findByPk(paymentIntent.invoice_id);
862
+ // Fetch subscription early for migration fallback support and overdraft protection check
863
+ const subscription = invoice?.subscription_id ? await Subscription.findByPk(invoice.subscription_id) : null;
862
864
 
863
865
  if (invoice?.status === 'void') {
864
866
  await paymentIntent.update({
@@ -870,20 +872,17 @@ export const handlePayment = async (job: PaymentJob) => {
870
872
  return;
871
873
  }
872
874
 
873
- if (invoice && invoice.subscription_id) {
874
- const subscription = await Subscription.findByPk(invoice.subscription_id);
875
- if (
876
- subscription &&
877
- subscription.isActive() &&
878
- subscription.overdraft_protection?.enabled &&
879
- invoice?.billing_reason === 'overdraft_protection'
880
- ) {
881
- logger.info('PaymentIntent capture skipped because of overdraft protection', {
882
- id: paymentIntent.id,
883
- subscription: subscription.id,
884
- });
885
- return;
886
- }
875
+ if (
876
+ subscription &&
877
+ subscription.isActive() &&
878
+ subscription.overdraft_protection?.enabled &&
879
+ invoice?.billing_reason === 'overdraft_protection'
880
+ ) {
881
+ logger.info('PaymentIntent capture skipped because of overdraft protection', {
882
+ id: paymentIntent.id,
883
+ subscription: subscription.id,
884
+ });
885
+ return;
887
886
  }
888
887
 
889
888
  // check max retry before doing any hard work
@@ -1006,18 +1005,19 @@ export const handlePayment = async (job: PaymentJob) => {
1006
1005
  paymentCurrency,
1007
1006
  userDid: payer || customer.did,
1008
1007
  amount: paymentIntent.amount,
1008
+ subscription,
1009
1009
  });
1010
1010
 
1011
1011
  if (result.sufficient === false) {
1012
1012
  logger.error('PaymentIntent capture aborted - insufficient balance/delegation', {
1013
1013
  paymentIntentId: paymentIntent.id,
1014
1014
  invoiceId: paymentIntent.invoice_id,
1015
- failureReason: 'INSUFFICIENT_BALANCE',
1015
+ failureReason: result.reason,
1016
1016
  result,
1017
1017
  queueDelayMs,
1018
1018
  delaySeconds: Math.round(queueDelayMs / 1000),
1019
1019
  });
1020
- throw new CustomError('INSUFFICIENT_BALANCE', 'Payer balance or delegation not sufficient for this payment');
1020
+ throw new CustomError(result.reason, 'Payer balance or delegation not sufficient for this payment');
1021
1021
  }
1022
1022
 
1023
1023
  const signed = await client.signTransferV2Tx({
@@ -1245,7 +1245,6 @@ export const handlePayment = async (job: PaymentJob) => {
1245
1245
  logger.warn('catch:PaymentIntent capture failed', { id: paymentIntent.id, attemptCount, minRetryMail, invoice });
1246
1246
 
1247
1247
  if (needSendMail && invoice.billing_reason === 'subscription_cycle') {
1248
- const subscription = await Subscription.findByPk(invoice.subscription_id);
1249
1248
  if (subscription) {
1250
1249
  await subscription.update({
1251
1250
  metadata: Object.assign(subscription.metadata, {
@@ -3,7 +3,7 @@ import { isRefundReasonSupportedByStripe } from '../libs/refund';
3
3
  import { checkRemainingStake, getSubscriptionStakeAddress } from '../libs/subscription';
4
4
  import { sendErc20ToUser } from '../integrations/ethereum/token';
5
5
  import { wallet } from '../libs/auth';
6
- import CustomError from '../libs/error';
6
+ import CustomError, { NonRetryableError } from '../libs/error';
7
7
  import { events } from '../libs/event';
8
8
  import logger from '../libs/logger';
9
9
  import { getGasPayerExtra, isBalanceSufficientForRefund } from '../libs/payment';
@@ -35,6 +35,21 @@ type Updates = {
35
35
  };
36
36
  };
37
37
 
38
+ const markRefundNonRetryable = async (refund: Refund, code: string, message: string, paymentMethod?: PaymentMethod) => {
39
+ await refund.update({
40
+ status: 'requires_action',
41
+ last_attempt_error: {
42
+ type: 'invalid_request_error',
43
+ code,
44
+ message,
45
+ payment_method_id: paymentMethod?.id,
46
+ payment_method_type: paymentMethod?.type,
47
+ },
48
+ attempt_count: refund.attempt_count + 1,
49
+ attempted: true,
50
+ });
51
+ };
52
+
38
53
  export const handleRefundFailed = (refund: Refund, error: PaymentError) => {
39
54
  const attemptCount = refund.attempt_count + 1;
40
55
  const updates: Updates = {
@@ -94,24 +109,27 @@ export const handleRefund = async (job: RefundJob) => {
94
109
  const paymentCurrency = await PaymentCurrency.findByPk(refund.currency_id);
95
110
  if (!paymentCurrency) {
96
111
  logger.warn(`PaymentCurrency not found: ${refund.currency_id}`);
112
+ await markRefundNonRetryable(refund, 'CURRENCY_NOT_FOUND', 'Payment currency not found');
97
113
  return;
98
114
  }
99
115
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
100
116
  if (!paymentMethod) {
101
117
  logger.warn(`PaymentMethod not found: ${paymentCurrency.payment_method_id}`);
118
+ await markRefundNonRetryable(refund, 'PAYMENT_METHOD_NOT_FOUND', 'Payment method not found');
102
119
  return;
103
120
  }
104
121
 
105
122
  const customer = await Customer.findByPk(refund.customer_id);
106
123
  if (!customer) {
107
124
  logger.warn(`Customer not found: ${refund.customer_id}`);
125
+ await markRefundNonRetryable(refund, 'CUSTOMER_NOT_FOUND', 'Customer not found', paymentMethod);
108
126
  return;
109
127
  }
110
128
 
111
129
  if (refund?.type === 'stake_return') {
112
- handleStakeReturnJob(job, refund, paymentCurrency, paymentMethod, customer);
130
+ await handleStakeReturnJob(job, refund, paymentCurrency, paymentMethod, customer);
113
131
  } else if (paymentMethod) {
114
- handleRefundJob(job, refund, paymentCurrency, paymentMethod, customer);
132
+ await handleRefundJob(job, refund, paymentCurrency, paymentMethod, customer);
115
133
  }
116
134
  };
117
135
 
@@ -164,18 +182,26 @@ const handleRefundJob = async (
164
182
  const refundSupport = ['arcblock', 'ethereum', 'stripe', 'base'].includes(paymentMethod.type);
165
183
  if (refundSupport === false) {
166
184
  logger.warn(`PaymentMethod does not support auto charge: ${paymentCurrency.payment_method_id}`);
185
+ await markRefundNonRetryable(
186
+ refund,
187
+ 'PAYMENT_METHOD_NOT_SUPPORTED',
188
+ 'Payment method does not support refund',
189
+ paymentMethod
190
+ );
167
191
  return;
168
192
  }
169
193
 
170
194
  // try refund transfer and reschedule on error
171
195
  logger.info('Refund transfer attempt', { id: refund.id, attempt: refund.attempt_count });
172
- let result;
173
- const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
174
- if (!paymentIntent) {
175
- throw new Error('PaymentIntent not found');
176
- }
177
-
178
196
  try {
197
+ if (!refund.payment_intent_id) {
198
+ throw new NonRetryableError('PAYMENT_INTENT_NOT_FOUND', 'payment_intent_id is missing for refund');
199
+ }
200
+ const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
201
+ if (!paymentIntent) {
202
+ throw new NonRetryableError('PAYMENT_INTENT_NOT_FOUND', 'PaymentIntent not found');
203
+ }
204
+ let result;
179
205
  if (paymentMethod.type === 'arcblock') {
180
206
  const client = paymentMethod.getOcapClient();
181
207
  // check balance before transfer with transaction
@@ -288,6 +314,12 @@ const handleRefundJob = async (
288
314
  } catch (err) {
289
315
  logger.error('refund transfer failed', { error: err, id: refund.id });
290
316
 
317
+ if (err instanceof NonRetryableError || err?.nonRetryable === true) {
318
+ await markRefundNonRetryable(refund, err.code, err.message, paymentMethod);
319
+ logger.error('refund transfer aborted: non-retryable error', { id: refund.id, code: err.code });
320
+ return;
321
+ }
322
+
291
323
  const error: PaymentError = {
292
324
  type: 'card_error',
293
325
  code: err.code,
@@ -348,7 +380,8 @@ const handleStakeReturnJob = async (
348
380
  const client = paymentMethod.getOcapClient();
349
381
  const subscription = await Subscription.findByPk(refund.subscription_id);
350
382
  const address =
351
- arcblockDetail?.staking?.address || (await getSubscriptionStakeAddress(subscription!, customer.did));
383
+ arcblockDetail?.staking?.address ||
384
+ (await getSubscriptionStakeAddress(subscription!, customer.did, paymentMethod));
352
385
  const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, refund.amount);
353
386
  if (!stakeEnough.enough) {
354
387
  logger.warn('Stake return aborted because stake is not enough ', {
@@ -814,6 +814,7 @@ const handleFinalInvoicePayment = async (
814
814
  paymentCurrency,
815
815
  userDid: payer || customer.did,
816
816
  amount: lastInvoice.amount_remaining,
817
+ subscription,
817
818
  });
818
819
 
819
820
  if (!hasSufficientBalance.sufficient) {
@@ -898,7 +899,7 @@ export const handleStakeSlashAfterCancel = async (subscription: Subscription, fo
898
899
 
899
900
  // check the staking
900
901
  const client = method.getOcapClient();
901
- const address = await getSubscriptionStakeAddress(subscription, customer.did);
902
+ const address = await getSubscriptionStakeAddress(subscription, customer.did, method);
902
903
  const { state } = await client.getStakeState({ address });
903
904
  if (!state || !state.data?.value) {
904
905
  logger.warn('Stake slashing aborted because no staking state', {
@@ -1142,7 +1143,7 @@ const slashStakeOnCancel = async (subscription: Subscription) => {
1142
1143
  });
1143
1144
  return;
1144
1145
  }
1145
- const address = await getSubscriptionStakeAddress(subscription, customer.did);
1146
+ const address = await getSubscriptionStakeAddress(subscription, customer.did, paymentMethod);
1146
1147
  const result = await getSubscriptionStakeSlashSetup(subscription, address, paymentMethod);
1147
1148
  const stakeEnough = await checkRemainingStake(paymentMethod, currency, address, result.return_amount);
1148
1149
  if (!stakeEnough.enough) {
@@ -365,6 +365,7 @@ async function checkSufficientBalance({
365
365
  paymentCurrency: rechargeCurrency,
366
366
  userDid: payer,
367
367
  amount: amount.toString(),
368
+ storedDelegationAddress: autoRechargeConfig.payment_details?.arcblock?.delegation_address,
368
369
  });
369
370
 
370
371
  return {
@@ -3133,7 +3133,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
3133
3133
  });
3134
3134
 
3135
3135
  const isPayment = checkoutSession.mode === 'payment';
3136
- let canFastPay = isPayment && canPayWithDelegation(paymentIntent?.beneficiaries || []);
3136
+ let canFastPay = isPayment && (await canPayWithDelegation(paymentIntent?.beneficiaries || [], paymentMethod));
3137
3137
  let fastPayInfo = null;
3138
3138
  let delegation: SufficientForPaymentResult | null = null;
3139
3139
  let creditSufficient = false;
@@ -3651,7 +3651,7 @@ router.post('/:id/fast-checkout-confirm', user, ensureCheckoutSessionOpen, async
3651
3651
 
3652
3652
  const isPayment = checkoutSession.mode === 'payment';
3653
3653
  let fastPaid = false;
3654
- let canFastPay = isPayment && canPayWithDelegation(paymentIntent?.beneficiaries || []);
3654
+ let canFastPay = isPayment && (await canPayWithDelegation(paymentIntent?.beneficiaries || [], paymentMethod));
3655
3655
  let delegation: SufficientForPaymentResult | null = null;
3656
3656
 
3657
3657
  // Handle credit payment directly
@@ -110,6 +110,7 @@ export default {
110
110
  type: 'delegate',
111
111
  payer: userDid,
112
112
  tx_hash: paymentDetails.tx_hash,
113
+ delegation_address: paymentDetails?.delegation_address,
113
114
  },
114
115
  },
115
116
  });
@@ -1631,6 +1631,7 @@ export async function executeOcapTransactions(
1631
1631
  tx_hash: delegationTxHash,
1632
1632
  payer: userDid,
1633
1633
  type: 'delegate',
1634
+ delegation_address: delegation?.meta?.address, // Store delegation address for migration compatibility
1634
1635
  staking: {
1635
1636
  tx_hash: stakingTxHash,
1636
1637
  address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
@@ -1592,6 +1592,7 @@ router.put('/:id', authPortal, async (req, res) => {
1592
1592
  paymentCurrency,
1593
1593
  userDid: payer,
1594
1594
  amount: due || '0',
1595
+ subscription,
1595
1596
  });
1596
1597
  logger.info('delegation sufficient for payment', {
1597
1598
  subscription: subscription.id,
@@ -2214,6 +2215,7 @@ router.put('/:id/slippage', authPortal, async (req, res) => {
2214
2215
  paymentCurrency,
2215
2216
  userDid: payer,
2216
2217
  amount: requiredAmount,
2218
+ subscription,
2217
2219
  });
2218
2220
 
2219
2221
  if (!delegation.sufficient) {
@@ -2485,6 +2487,7 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
2485
2487
  paymentCurrency,
2486
2488
  userDid: payer,
2487
2489
  amount: requiredAmount,
2490
+ subscription,
2488
2491
  });
2489
2492
  const noStake = subscription.billing_thresholds?.no_stake;
2490
2493
  if (paymentMethod.type === 'arcblock' && delegation.sufficient && !noStake) {
@@ -2901,6 +2904,7 @@ router.get('/:id/delegation', authPortal, async (req, res) => {
2901
2904
  paymentCurrency: subscription.paymentCurrency,
2902
2905
  userDid: payer as string,
2903
2906
  amount: '0',
2907
+ subscription,
2904
2908
  });
2905
2909
  if (
2906
2910
  !delegator.sufficient &&
@@ -330,6 +330,7 @@ export type PaymentDetails = {
330
330
  payer: string;
331
331
  type?: LiteralUnion<'slash' | 'transfer' | 'delegate' | 'stake_return' | 'credit', string>;
332
332
  receiver?: string;
333
+ delegation_address?: string; // The delegation address at the time of authorization (for migration compatibility)
333
334
  staking?: {
334
335
  tx_hash: string;
335
336
  address: string;
@@ -0,0 +1,271 @@
1
+ import { toDelegateAddress, toStakeAddress } from '@arcblock/did-util';
2
+
3
+ import {
4
+ getMigratedFromList,
5
+ getDelegationAddressWithFallback,
6
+ getStakingAddressWithFallback,
7
+ getAppStakeAddressWithFallback,
8
+ clearMigratedFromCache,
9
+ } from '../../src/libs/wallet-migration';
10
+
11
+ // Mock the auth module
12
+ jest.mock('../../src/libs/auth', () => ({
13
+ wallet: {
14
+ address: 'zNKcF7wyrvAvn4YaADjr4p9gpFUSHnW2aQJa', // Current app DID
15
+ },
16
+ }));
17
+
18
+ // Mock logger
19
+ jest.mock('../../src/libs/logger', () => ({
20
+ info: jest.fn(),
21
+ warn: jest.fn(),
22
+ error: jest.fn(),
23
+ }));
24
+
25
+ describe('wallet-migration', () => {
26
+ // Mock OcapClient
27
+ const createMockClient = (overrides: any = {}) => ({
28
+ getAccountState: jest.fn().mockResolvedValue({
29
+ state: {
30
+ migratedFrom: ['zNKq13Dr2TBHELpLDUJFGxepiGP7YHbAVxPn', 'zNKq6yG8AVbwdRBDJSNCDVJJUQD1AXdkK7Wp'],
31
+ address: 'zNKcF7wyrvAvn4YaADjr4p9gpFUSHnW2aQJa',
32
+ },
33
+ }),
34
+ getDelegateState: jest.fn().mockResolvedValue({ state: null }),
35
+ getStakeState: jest.fn().mockResolvedValue({ state: null }),
36
+ ...overrides,
37
+ });
38
+
39
+ beforeEach(() => {
40
+ clearMigratedFromCache();
41
+ });
42
+
43
+ describe('getMigratedFromList', () => {
44
+ it('should return migratedFrom list from chain', async () => {
45
+ const mockClient = createMockClient();
46
+ const result = await getMigratedFromList(mockClient as any);
47
+ expect(result).toEqual(['zNKq13Dr2TBHELpLDUJFGxepiGP7YHbAVxPn', 'zNKq6yG8AVbwdRBDJSNCDVJJUQD1AXdkK7Wp']);
48
+ });
49
+
50
+ it('should cache the result and not query again', async () => {
51
+ const mockClient = createMockClient();
52
+ await getMigratedFromList(mockClient as any);
53
+ await getMigratedFromList(mockClient as any);
54
+ expect(mockClient.getAccountState).toHaveBeenCalledTimes(1);
55
+ });
56
+
57
+ it('should return empty array if no migratedFrom', async () => {
58
+ const mockClient = createMockClient({
59
+ getAccountState: jest.fn().mockResolvedValue({ state: {} }),
60
+ });
61
+ const result = await getMigratedFromList(mockClient as any);
62
+ expect(result).toEqual([]);
63
+ });
64
+
65
+ it('should return empty array on error', async () => {
66
+ const mockClient = createMockClient({
67
+ getAccountState: jest.fn().mockRejectedValue(new Error('Network error')),
68
+ });
69
+ const result = await getMigratedFromList(mockClient as any);
70
+ expect(result).toEqual([]);
71
+ });
72
+ });
73
+
74
+ describe('getDelegationAddressWithFallback', () => {
75
+ const delegator = 'zNKtCNqYWLYWYW3gWRA1vnRNdWLrFXfPxRKW';
76
+ const currentWalletAddress = 'zNKcF7wyrvAvn4YaADjr4p9gpFUSHnW2aQJa';
77
+ const oldAppDid = 'zNKq13Dr2TBHELpLDUJFGxepiGP7YHbAVxPn';
78
+
79
+ it('should return stored address if available', async () => {
80
+ const mockClient = createMockClient();
81
+
82
+ const result = await getDelegationAddressWithFallback({
83
+ storedAddress: 'stored_delegation_address',
84
+ delegator,
85
+ client: mockClient as any,
86
+ });
87
+
88
+ expect(result).toEqual({
89
+ address: 'stored_delegation_address',
90
+ needsBackfill: false,
91
+ source: 'stored',
92
+ });
93
+ // Should not query chain if stored
94
+ expect(mockClient.getDelegateState).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it('should return current address if delegation exists on current wallet', async () => {
98
+ const currentAddress = toDelegateAddress(delegator, currentWalletAddress);
99
+ const mockClient = createMockClient({
100
+ getDelegateState: jest.fn().mockImplementation(({ address }) => {
101
+ if (address === currentAddress) {
102
+ return { state: { ops: [{ key: 'fg:x:transfer' }] } };
103
+ }
104
+ return { state: null };
105
+ }),
106
+ });
107
+
108
+ const result = await getDelegationAddressWithFallback({
109
+ delegator,
110
+ client: mockClient as any,
111
+ });
112
+
113
+ expect(result).toEqual({
114
+ address: currentAddress,
115
+ needsBackfill: true,
116
+ source: 'current',
117
+ });
118
+ });
119
+
120
+ it('should fallback to migratedFrom addresses', async () => {
121
+ const oldAddress = toDelegateAddress(delegator, oldAppDid);
122
+ const mockClient = createMockClient({
123
+ getDelegateState: jest.fn().mockImplementation(({ address }) => {
124
+ if (address === oldAddress) {
125
+ return { state: { ops: [{ key: 'fg:x:transfer' }] } };
126
+ }
127
+ return { state: null };
128
+ }),
129
+ });
130
+
131
+ const result = await getDelegationAddressWithFallback({
132
+ delegator,
133
+ client: mockClient as any,
134
+ });
135
+
136
+ expect(result).toEqual({
137
+ address: oldAddress,
138
+ needsBackfill: true,
139
+ source: 'migrated',
140
+ });
141
+ });
142
+
143
+ it('should return null if no delegation found', async () => {
144
+ const mockClient = createMockClient();
145
+
146
+ const result = await getDelegationAddressWithFallback({
147
+ delegator,
148
+ client: mockClient as any,
149
+ });
150
+
151
+ expect(result).toBeNull();
152
+ });
153
+ });
154
+
155
+ describe('getStakingAddressWithFallback', () => {
156
+ const userDid = 'zNKtCNqYWLYWYW3gWRA1vnRNdWLrFXfPxRKW';
157
+ const nonce = 'sub_123';
158
+ const currentWalletAddress = 'zNKcF7wyrvAvn4YaADjr4p9gpFUSHnW2aQJa';
159
+ const oldAppDid = 'zNKq13Dr2TBHELpLDUJFGxepiGP7YHbAVxPn';
160
+
161
+ it('should return stored address if available', async () => {
162
+ const mockClient = createMockClient();
163
+
164
+ const result = await getStakingAddressWithFallback({
165
+ storedAddress: 'stored_stake_address',
166
+ userDid,
167
+ nonce,
168
+ client: mockClient as any,
169
+ });
170
+
171
+ expect(result).toEqual({
172
+ address: 'stored_stake_address',
173
+ needsBackfill: false,
174
+ source: 'stored',
175
+ });
176
+ });
177
+
178
+ it('should return current address if stake exists on current wallet', async () => {
179
+ const currentAddress = toStakeAddress(userDid, currentWalletAddress, nonce);
180
+ const mockClient = createMockClient({
181
+ getStakeState: jest.fn().mockImplementation(({ address }) => {
182
+ if (address === currentAddress) {
183
+ return { state: { tokens: [] } };
184
+ }
185
+ return { state: null };
186
+ }),
187
+ });
188
+
189
+ const result = await getStakingAddressWithFallback({
190
+ userDid,
191
+ nonce,
192
+ client: mockClient as any,
193
+ });
194
+
195
+ expect(result).toEqual({
196
+ address: currentAddress,
197
+ needsBackfill: true,
198
+ source: 'current',
199
+ });
200
+ });
201
+
202
+ it('should fallback to migratedFrom addresses', async () => {
203
+ const oldAddress = toStakeAddress(userDid, oldAppDid, nonce);
204
+ const mockClient = createMockClient({
205
+ getStakeState: jest.fn().mockImplementation(({ address }) => {
206
+ if (address === oldAddress) {
207
+ return { state: { tokens: [] } };
208
+ }
209
+ return { state: null };
210
+ }),
211
+ });
212
+
213
+ const result = await getStakingAddressWithFallback({
214
+ userDid,
215
+ nonce,
216
+ client: mockClient as any,
217
+ });
218
+
219
+ expect(result).toEqual({
220
+ address: oldAddress,
221
+ needsBackfill: true,
222
+ source: 'migrated',
223
+ });
224
+ });
225
+ });
226
+
227
+ describe('getAppStakeAddressWithFallback', () => {
228
+ const currentWalletAddress = 'zNKcF7wyrvAvn4YaADjr4p9gpFUSHnW2aQJa';
229
+ const oldAppDid = 'zNKq13Dr2TBHELpLDUJFGxepiGP7YHbAVxPn';
230
+
231
+ it('should return current address if app stake exists', async () => {
232
+ const currentAddress = toStakeAddress(currentWalletAddress, currentWalletAddress);
233
+ const mockClient = createMockClient({
234
+ getStakeState: jest.fn().mockImplementation(({ address }) => {
235
+ if (address === currentAddress) {
236
+ return { state: { tokens: [] } };
237
+ }
238
+ return { state: null };
239
+ }),
240
+ });
241
+
242
+ const result = await getAppStakeAddressWithFallback({ client: mockClient as any });
243
+
244
+ expect(result).toEqual({
245
+ address: currentAddress,
246
+ needsBackfill: false,
247
+ source: 'current',
248
+ });
249
+ });
250
+
251
+ it('should fallback to migratedFrom addresses for app stake', async () => {
252
+ const oldAddress = toStakeAddress(oldAppDid, oldAppDid);
253
+ const mockClient = createMockClient({
254
+ getStakeState: jest.fn().mockImplementation(({ address }) => {
255
+ if (address === oldAddress) {
256
+ return { state: { tokens: [] } };
257
+ }
258
+ return { state: null };
259
+ }),
260
+ });
261
+
262
+ const result = await getAppStakeAddressWithFallback({ client: mockClient as any });
263
+
264
+ expect(result).toEqual({
265
+ address: oldAddress,
266
+ needsBackfill: false,
267
+ source: 'migrated',
268
+ });
269
+ });
270
+ });
271
+ });
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.25.0
17
+ version: 1.25.2
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.25.0",
3
+ "version": "1.25.2",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "prelint": "npm run types",
@@ -59,9 +59,9 @@
59
59
  "@blocklet/error": "^0.3.5",
60
60
  "@blocklet/js-sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
61
61
  "@blocklet/logger": "^1.17.8-beta-20260104-120132-cb5b1914",
62
- "@blocklet/payment-broker-client": "1.25.0",
63
- "@blocklet/payment-react": "1.25.0",
64
- "@blocklet/payment-vendor": "1.25.0",
62
+ "@blocklet/payment-broker-client": "1.25.2",
63
+ "@blocklet/payment-react": "1.25.2",
64
+ "@blocklet/payment-vendor": "1.25.2",
65
65
  "@blocklet/sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
66
66
  "@blocklet/ui-react": "^3.4.7",
67
67
  "@blocklet/uploader": "^0.3.19",
@@ -132,7 +132,7 @@
132
132
  "devDependencies": {
133
133
  "@abtnode/types": "^1.17.8-beta-20260104-120132-cb5b1914",
134
134
  "@arcblock/eslint-config-ts": "^0.3.3",
135
- "@blocklet/payment-types": "1.25.0",
135
+ "@blocklet/payment-types": "1.25.2",
136
136
  "@types/cookie-parser": "^1.4.9",
137
137
  "@types/cors": "^2.8.19",
138
138
  "@types/debug": "^4.1.12",
@@ -179,5 +179,5 @@
179
179
  "parser": "typescript"
180
180
  }
181
181
  },
182
- "gitHead": "00a94943fea21487c042a7b484f4649add502bc9"
182
+ "gitHead": "11621a1c925f94bf4f73644de70088e5f0580f74"
183
183
  }