payment-kit 1.22.31 → 1.23.0
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/api/src/index.ts +4 -0
- package/api/src/integrations/arcblock/token.ts +599 -0
- package/api/src/libs/credit-grant.ts +7 -6
- package/api/src/queues/credit-consume.ts +29 -4
- package/api/src/queues/credit-grant.ts +245 -50
- package/api/src/queues/credit-reconciliation.ts +253 -0
- package/api/src/queues/refund.ts +263 -30
- package/api/src/queues/token-transfer.ts +331 -0
- package/api/src/routes/checkout-sessions.ts +1 -1
- package/api/src/routes/credit-grants.ts +27 -7
- package/api/src/routes/credit-tokens.ts +38 -0
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/meter-events.ts +1 -1
- package/api/src/routes/meters.ts +32 -10
- package/api/src/routes/payment-currencies.ts +103 -0
- package/api/src/routes/products.ts +2 -2
- package/api/src/routes/settings.ts +4 -3
- package/api/src/store/migrations/20251120-add-token-config-to-currencies.ts +20 -0
- package/api/src/store/migrations/20251204-add-chain-fields.ts +74 -0
- package/api/src/store/models/credit-grant.ts +57 -10
- package/api/src/store/models/credit-transaction.ts +18 -1
- package/api/src/store/models/meter-event.ts +48 -25
- package/api/src/store/models/payment-currency.ts +31 -4
- package/api/src/store/models/refund.ts +12 -2
- package/api/src/store/models/types.ts +48 -0
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/components/customer/credit-overview.tsx +1 -1
- package/src/components/meter/form.tsx +191 -18
- package/src/components/price/form.tsx +49 -37
- package/src/locales/en.tsx +24 -0
- package/src/locales/zh.tsx +26 -0
- package/src/pages/admin/billing/meters/create.tsx +42 -13
- package/src/pages/admin/billing/meters/detail.tsx +56 -5
|
@@ -1,23 +1,54 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import { FormInput, FormLabel, Collapse } from '@blocklet/payment-react';
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { FormInput, FormLabel, Collapse, usePaymentContext } from '@blocklet/payment-react';
|
|
3
|
+
import {
|
|
4
|
+
FormControl,
|
|
5
|
+
Select,
|
|
6
|
+
MenuItem,
|
|
7
|
+
Typography,
|
|
8
|
+
Box,
|
|
9
|
+
FormHelperText,
|
|
10
|
+
Stack,
|
|
11
|
+
Radio,
|
|
12
|
+
RadioGroup,
|
|
13
|
+
FormControlLabel,
|
|
14
|
+
Switch,
|
|
15
|
+
Autocomplete,
|
|
16
|
+
TextField,
|
|
17
|
+
} from '@mui/material';
|
|
18
|
+
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
|
5
19
|
import { InfoOutlined } from '@mui/icons-material';
|
|
20
|
+
import { useRequest } from 'ahooks';
|
|
21
|
+
import { useState, useMemo } from 'react';
|
|
22
|
+
import GraphQLClient from '@ocap/client';
|
|
6
23
|
|
|
7
|
-
import type { TMeter } from '@blocklet/payment-types';
|
|
24
|
+
import type { TMeter, TPaymentCurrency } from '@blocklet/payment-types';
|
|
8
25
|
import MetadataForm from '../metadata/form';
|
|
9
26
|
|
|
27
|
+
export type TokenConfig = { name: string; symbol: string; tokenFactoryAddress: string };
|
|
28
|
+
|
|
10
29
|
type Props = {
|
|
11
30
|
mode?: 'create' | 'edit';
|
|
31
|
+
tokenConfig?: TokenConfig;
|
|
32
|
+
onTokenConfigChange?: (next: TokenConfig) => void;
|
|
33
|
+
paymentCurrency?: TPaymentCurrency;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type TMeterForm = TMeter & {
|
|
37
|
+
mode?: 'OffChain' | 'OnChain';
|
|
12
38
|
};
|
|
13
39
|
|
|
14
|
-
function MeterForm({
|
|
40
|
+
function MeterForm({
|
|
41
|
+
mode = 'create',
|
|
42
|
+
tokenConfig = undefined,
|
|
43
|
+
onTokenConfigChange = undefined,
|
|
44
|
+
paymentCurrency = undefined,
|
|
45
|
+
}: Props) {
|
|
15
46
|
const { t } = useLocaleContext();
|
|
16
47
|
const {
|
|
17
48
|
register,
|
|
18
49
|
control,
|
|
19
50
|
formState: { errors },
|
|
20
|
-
} = useFormContext<
|
|
51
|
+
} = useFormContext<TMeterForm>();
|
|
21
52
|
|
|
22
53
|
// 监听聚合方法的变化,用于显示动态帮助信息
|
|
23
54
|
const aggregationMethod = useWatch({
|
|
@@ -26,20 +57,52 @@ function MeterForm({ mode = 'create' }: Props) {
|
|
|
26
57
|
defaultValue: 'sum',
|
|
27
58
|
});
|
|
28
59
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return t('admin.meter.aggregationMethod.lastDescription');
|
|
37
|
-
default:
|
|
38
|
-
return '';
|
|
39
|
-
}
|
|
40
|
-
};
|
|
60
|
+
const creditMode = useWatch({
|
|
61
|
+
control,
|
|
62
|
+
name: 'mode',
|
|
63
|
+
defaultValue: 'OffChain',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const [tokenType, setTokenType] = useState<'new' | 'existing'>(tokenConfig?.tokenFactoryAddress ? 'existing' : 'new');
|
|
41
67
|
|
|
42
68
|
const isEditing = mode === 'edit';
|
|
69
|
+
const { settings } = usePaymentContext();
|
|
70
|
+
|
|
71
|
+
// Check if current currency already has token_config
|
|
72
|
+
const hasTokenConfig = !!paymentCurrency?.token_config;
|
|
73
|
+
|
|
74
|
+
// Get GraphQL client for arcblock chain
|
|
75
|
+
const arcblockClient = useMemo(() => {
|
|
76
|
+
const arcblockMethod = settings.paymentMethods?.find((m) => m.type === 'arcblock') as any;
|
|
77
|
+
const apiHost = arcblockMethod?.api_host;
|
|
78
|
+
return apiHost ? new GraphQLClient(apiHost) : null;
|
|
79
|
+
}, [settings.paymentMethods]);
|
|
80
|
+
|
|
81
|
+
// Fetch tokens created by blocklet owner
|
|
82
|
+
const { data: tokens = [] } = useRequest(
|
|
83
|
+
async () => {
|
|
84
|
+
if (!arcblockClient || !window.blocklet?.appId) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const result = await arcblockClient.listTokens({
|
|
88
|
+
issuerAddress: window.blocklet.appId,
|
|
89
|
+
});
|
|
90
|
+
return result?.tokens || [];
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
refreshDeps: [creditMode, arcblockClient],
|
|
94
|
+
ready: creditMode === 'OnChain' && !hasTokenConfig && !!arcblockClient,
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const getAggregationDescription = (method: string) => {
|
|
99
|
+
const map: Record<string, string> = {
|
|
100
|
+
sum: t('admin.meter.aggregationMethod.sumDescription'),
|
|
101
|
+
count: t('admin.meter.aggregationMethod.countDescription'),
|
|
102
|
+
last: t('admin.meter.aggregationMethod.lastDescription'),
|
|
103
|
+
};
|
|
104
|
+
return map[method] || '';
|
|
105
|
+
};
|
|
43
106
|
|
|
44
107
|
return (
|
|
45
108
|
<Box sx={{ maxWidth: 600 }}>
|
|
@@ -223,6 +286,116 @@ function MeterForm({ mode = 'create' }: Props) {
|
|
|
223
286
|
</FormHelperText>
|
|
224
287
|
</Box>
|
|
225
288
|
|
|
289
|
+
{/* On-chain token configuration */}
|
|
290
|
+
<Box sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
|
|
291
|
+
<Controller
|
|
292
|
+
name="mode"
|
|
293
|
+
control={control}
|
|
294
|
+
render={({ field }) => (
|
|
295
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
296
|
+
<Box>
|
|
297
|
+
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
|
298
|
+
{t('admin.meter.creditMode.onchain')}
|
|
299
|
+
</Typography>
|
|
300
|
+
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
|
301
|
+
{t('admin.meter.creditMode.onChainDescription')}
|
|
302
|
+
</Typography>
|
|
303
|
+
</Box>
|
|
304
|
+
<Switch
|
|
305
|
+
checked={hasTokenConfig ? field.value !== 'OffChain' : field.value === 'OnChain'}
|
|
306
|
+
onChange={(e) => field.onChange(e.target.checked ? 'OnChain' : 'OffChain')}
|
|
307
|
+
/>
|
|
308
|
+
</Stack>
|
|
309
|
+
)}
|
|
310
|
+
/>
|
|
311
|
+
|
|
312
|
+
{(hasTokenConfig || creditMode === 'OnChain') && (
|
|
313
|
+
<Box sx={{ mt: 3, pt: 3, borderTop: '1px solid', borderColor: 'divider' }}>
|
|
314
|
+
<Typography variant="subtitle2" sx={{ mb: 2 }}>
|
|
315
|
+
{t('admin.meter.creditMode.tokenConfiguration')}
|
|
316
|
+
</Typography>
|
|
317
|
+
{!hasTokenConfig && (
|
|
318
|
+
<RadioGroup
|
|
319
|
+
value={tokenType}
|
|
320
|
+
onChange={(e) => {
|
|
321
|
+
setTokenType(e.target.value as any);
|
|
322
|
+
onTokenConfigChange?.({ name: '', symbol: '', tokenFactoryAddress: '' });
|
|
323
|
+
}}
|
|
324
|
+
row>
|
|
325
|
+
<FormControlLabel
|
|
326
|
+
value="new"
|
|
327
|
+
control={<Radio />}
|
|
328
|
+
label={t('admin.meter.creditMode.createNewToken')}
|
|
329
|
+
/>
|
|
330
|
+
<FormControlLabel
|
|
331
|
+
value="existing"
|
|
332
|
+
control={<Radio />}
|
|
333
|
+
label={t('admin.meter.creditMode.useExistingToken')}
|
|
334
|
+
/>
|
|
335
|
+
</RadioGroup>
|
|
336
|
+
)}
|
|
337
|
+
{!hasTokenConfig && tokenType === 'new' && tokenConfig && onTokenConfigChange ? (
|
|
338
|
+
<Box sx={{ mt: 2 }}>
|
|
339
|
+
<TextField
|
|
340
|
+
value={tokenConfig.name}
|
|
341
|
+
onChange={(e) => onTokenConfigChange({ ...tokenConfig, name: e.target.value })}
|
|
342
|
+
fullWidth
|
|
343
|
+
label={t('admin.meter.creditMode.tokenName')}
|
|
344
|
+
inputProps={{ maxLength: 64 }}
|
|
345
|
+
sx={{ mb: 2 }}
|
|
346
|
+
/>
|
|
347
|
+
<TextField
|
|
348
|
+
value={tokenConfig.symbol}
|
|
349
|
+
onChange={(e) => onTokenConfigChange({ ...tokenConfig, symbol: e.target.value.toUpperCase() })}
|
|
350
|
+
fullWidth
|
|
351
|
+
label={t('admin.meter.creditMode.tokenSymbol')}
|
|
352
|
+
inputProps={{ maxLength: 6, style: { textTransform: 'uppercase', fontFamily: 'monospace' } }}
|
|
353
|
+
/>
|
|
354
|
+
</Box>
|
|
355
|
+
) : (
|
|
356
|
+
<Box sx={{ mt: 2 }}>
|
|
357
|
+
{hasTokenConfig && paymentCurrency?.token_config ? (
|
|
358
|
+
<TextField
|
|
359
|
+
fullWidth
|
|
360
|
+
label={t('admin.meter.creditMode.selectToken')}
|
|
361
|
+
value={`${paymentCurrency.token_config.symbol} (${paymentCurrency.token_config.name})`}
|
|
362
|
+
disabled
|
|
363
|
+
/>
|
|
364
|
+
) : (
|
|
365
|
+
<Autocomplete
|
|
366
|
+
options={tokens}
|
|
367
|
+
getOptionLabel={(o: any) => (typeof o === 'string' ? o : `${o.symbol} (${o.name})`)}
|
|
368
|
+
value={
|
|
369
|
+
tokens.find((f: any) => f.tokenFactoryAddress === tokenConfig?.tokenFactoryAddress) || null
|
|
370
|
+
}
|
|
371
|
+
onChange={(_, v: any) =>
|
|
372
|
+
onTokenConfigChange?.({
|
|
373
|
+
name: v?.name || '',
|
|
374
|
+
symbol: v?.symbol || '',
|
|
375
|
+
tokenFactoryAddress: v?.tokenFactoryAddress || '',
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
renderInput={(params) => (
|
|
379
|
+
<TextField {...params} label={t('admin.meter.creditMode.selectToken')} />
|
|
380
|
+
)}
|
|
381
|
+
renderOption={(props, o: any) => (
|
|
382
|
+
<li {...props}>
|
|
383
|
+
<Box>
|
|
384
|
+
<Typography variant="body1">{o.symbol}</Typography>
|
|
385
|
+
<Typography variant="caption" color="text.secondary">
|
|
386
|
+
{o.name}
|
|
387
|
+
</Typography>
|
|
388
|
+
</Box>
|
|
389
|
+
</li>
|
|
390
|
+
)}
|
|
391
|
+
/>
|
|
392
|
+
)}
|
|
393
|
+
</Box>
|
|
394
|
+
)}
|
|
395
|
+
</Box>
|
|
396
|
+
)}
|
|
397
|
+
</Box>
|
|
398
|
+
|
|
226
399
|
{/* Metadata Section */}
|
|
227
400
|
<Collapse trigger={t('common.metadata.label')}>
|
|
228
401
|
<Box sx={{ mb: 2 }}>
|
|
@@ -835,65 +835,77 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
835
835
|
/>
|
|
836
836
|
|
|
837
837
|
{/* 可用时长配置 */}
|
|
838
|
-
<
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
838
|
+
<Controller
|
|
839
|
+
name={getFieldName('metadata')}
|
|
840
|
+
control={control}
|
|
841
|
+
render={({ field }) => (
|
|
842
|
+
<Box sx={{ width: '100%', mb: 2 }}>
|
|
843
|
+
<FormLabel tooltip={t('admin.creditProduct.validDuration.help')}>
|
|
844
|
+
{t('admin.creditProduct.validDuration.label')}
|
|
845
|
+
</FormLabel>
|
|
846
|
+
<Stack
|
|
847
|
+
direction="row"
|
|
848
|
+
spacing={1}
|
|
849
|
+
sx={{
|
|
850
|
+
alignItems: 'center',
|
|
851
|
+
}}>
|
|
852
852
|
<TextField
|
|
853
|
-
{...field}
|
|
854
853
|
size="small"
|
|
855
854
|
type="number"
|
|
856
855
|
sx={{ flex: 1 }}
|
|
857
|
-
value={field.value ?? '0'}
|
|
856
|
+
value={field.value?.credit_config?.valid_duration_value ?? '0'}
|
|
858
857
|
placeholder="0"
|
|
859
858
|
disabled={isLocked}
|
|
859
|
+
onChange={(e) => {
|
|
860
|
+
const metadata = field.value || {};
|
|
861
|
+
const creditConfig = metadata.credit_config || {};
|
|
862
|
+
const value = +e.target.value;
|
|
863
|
+
if (!Number.isNaN(value) && value >= 0) {
|
|
864
|
+
creditConfig.valid_duration_value = +value;
|
|
865
|
+
} else {
|
|
866
|
+
delete creditConfig.valid_duration_value;
|
|
867
|
+
}
|
|
868
|
+
metadata.credit_config = creditConfig;
|
|
869
|
+
field.onChange(metadata);
|
|
870
|
+
}}
|
|
860
871
|
slotProps={{
|
|
861
872
|
htmlInput: {
|
|
862
873
|
min: 0,
|
|
874
|
+
step: 0.1,
|
|
863
875
|
},
|
|
864
876
|
}}
|
|
865
877
|
/>
|
|
866
|
-
)}
|
|
867
|
-
/>
|
|
868
|
-
<Controller
|
|
869
|
-
name={getFieldName('metadata.credit_config.valid_duration_unit')}
|
|
870
|
-
control={control}
|
|
871
|
-
render={({ field }) => (
|
|
872
878
|
<Select
|
|
873
|
-
{...field}
|
|
874
879
|
size="small"
|
|
875
880
|
sx={{ minWidth: 120 }}
|
|
876
881
|
disabled={isLocked}
|
|
877
|
-
value={field.value || 'days'}
|
|
882
|
+
value={field.value?.credit_config?.valid_duration_unit || 'days'}
|
|
883
|
+
onChange={(e) => {
|
|
884
|
+
const metadata = field.value || {};
|
|
885
|
+
const creditConfig = metadata.credit_config || {};
|
|
886
|
+
creditConfig.valid_duration_unit = e.target.value;
|
|
887
|
+
metadata.credit_config = creditConfig;
|
|
888
|
+
field.onChange(metadata);
|
|
889
|
+
}}>
|
|
878
890
|
{!livemode && <MenuItem value="hours">{t('admin.creditProduct.validDuration.hours')}</MenuItem>}
|
|
879
891
|
<MenuItem value="days">{t('admin.creditProduct.validDuration.days')}</MenuItem>
|
|
880
892
|
<MenuItem value="weeks">{t('admin.creditProduct.validDuration.weeks')}</MenuItem>
|
|
881
893
|
<MenuItem value="months">{t('admin.creditProduct.validDuration.months')}</MenuItem>
|
|
882
894
|
<MenuItem value="years">{t('admin.creditProduct.validDuration.years')}</MenuItem>
|
|
883
895
|
</Select>
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
896
|
+
</Stack>
|
|
897
|
+
<Typography
|
|
898
|
+
variant="caption"
|
|
899
|
+
sx={{
|
|
900
|
+
color: 'text.secondary',
|
|
901
|
+
display: 'block',
|
|
902
|
+
mt: 0.5,
|
|
903
|
+
}}>
|
|
904
|
+
{t('admin.creditProduct.validDuration.description')}
|
|
905
|
+
</Typography>
|
|
906
|
+
</Box>
|
|
907
|
+
)}
|
|
908
|
+
/>
|
|
897
909
|
|
|
898
910
|
{/* 关联特定价格 */}
|
|
899
911
|
<Controller
|
package/src/locales/en.tsx
CHANGED
|
@@ -273,6 +273,30 @@ export default flat({
|
|
|
273
273
|
edit: 'Edit meter',
|
|
274
274
|
save: 'Save meter',
|
|
275
275
|
saved: 'Meter successfully saved',
|
|
276
|
+
creditMode: {
|
|
277
|
+
onchain: 'OnChain',
|
|
278
|
+
onChainDescription: 'Once enabled, your credit will be published as a Token on the ArcBlock blockchain',
|
|
279
|
+
tokenConfiguration: 'Token Configuration',
|
|
280
|
+
createNewToken: 'Create New Token',
|
|
281
|
+
useExistingToken: 'Use Existing Token',
|
|
282
|
+
createTokenHint: 'Fill in the token name and symbol to create a new Token',
|
|
283
|
+
tokenName: 'Token Name',
|
|
284
|
+
tokenNamePlaceholder: 'Enter token name',
|
|
285
|
+
tokenNameRequired: 'Token name is required',
|
|
286
|
+
tokenSymbol: 'Token Symbol',
|
|
287
|
+
tokenSymbolPlaceholder: 'Enter token symbol',
|
|
288
|
+
tokenSymbolRequired: 'Token symbol is required',
|
|
289
|
+
tokenSymbolFormat: 'Symbol must be 1-6 uppercase letters or numbers',
|
|
290
|
+
createTokenButton: 'Create Token',
|
|
291
|
+
creatingToken: 'Creating...',
|
|
292
|
+
tokenCreated: 'Token created successfully',
|
|
293
|
+
tokenCreatedLabel: 'Token Created',
|
|
294
|
+
tokenAddress: 'Token Address',
|
|
295
|
+
selectToken: 'Select Token',
|
|
296
|
+
selectTokenPlaceholder: 'Select an existing token',
|
|
297
|
+
tokenAddressRequired: 'Token address is required',
|
|
298
|
+
enabled: 'Enabled',
|
|
299
|
+
},
|
|
276
300
|
activate: 'Activate meter',
|
|
277
301
|
activated: 'Meter activated successfully',
|
|
278
302
|
deactivate: 'Deactivate meter',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -283,6 +283,32 @@ export default flat({
|
|
|
283
283
|
basicInfo: '基本信息',
|
|
284
284
|
basicInfoDescription: '配置计量器的核心设置以跟踪使用事件。',
|
|
285
285
|
editDescription: '更新计量器名称、描述和元数据。事件名称和聚合方法等核心设置无法更改。',
|
|
286
|
+
|
|
287
|
+
creditMode: {
|
|
288
|
+
onchain: 'OnChain',
|
|
289
|
+
onChainDescription:
|
|
290
|
+
'开启后你的 Credit 将作为 Token 发布在 ArcBlock 区块链上,用户的充值、退款和消费将在链上公开记录且不可篡改。',
|
|
291
|
+
tokenConfiguration: 'Token 配置',
|
|
292
|
+
createNewToken: '创建新 Token',
|
|
293
|
+
useExistingToken: '使用现有 Token',
|
|
294
|
+
createTokenHint: '填写 Token 名称和符号以创建新 Token',
|
|
295
|
+
tokenName: 'Token 名称',
|
|
296
|
+
tokenNamePlaceholder: '请输入 Token 名称',
|
|
297
|
+
tokenNameRequired: 'Token 名称不能为空',
|
|
298
|
+
tokenSymbol: 'Token 符号',
|
|
299
|
+
tokenSymbolPlaceholder: '请输入 Token 符号',
|
|
300
|
+
tokenSymbolRequired: 'Token 符号不能为空',
|
|
301
|
+
tokenSymbolFormat: '符号必须为 1-6 个大写字母或数字',
|
|
302
|
+
createTokenButton: '创建 Token',
|
|
303
|
+
creatingToken: '创建中...',
|
|
304
|
+
tokenCreated: 'Token 创建成功',
|
|
305
|
+
tokenCreatedLabel: 'Token 已创建',
|
|
306
|
+
tokenAddress: 'Token 地址',
|
|
307
|
+
selectToken: '选择 Token',
|
|
308
|
+
selectTokenPlaceholder: '请选择一个现有的 Token',
|
|
309
|
+
tokenAddressRequired: 'Token 地址不能为空',
|
|
310
|
+
enabled: '已启用',
|
|
311
|
+
},
|
|
286
312
|
inactive: '计量器未激活',
|
|
287
313
|
inactiveTip: '此计量器未收集使用数据。激活它以开始跟踪事件。',
|
|
288
314
|
name: {
|
|
@@ -5,15 +5,24 @@ import { api, formatError, usePaymentContext } from '@blocklet/payment-react';
|
|
|
5
5
|
import { useForm, FormProvider } from 'react-hook-form';
|
|
6
6
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
7
7
|
import { dispatch } from 'use-bus';
|
|
8
|
+
import { useLocalStorageState } from 'ahooks';
|
|
8
9
|
|
|
9
10
|
import type { TMeter } from '@blocklet/payment-types';
|
|
10
|
-
import MeterForm from '../../../../components/meter/form';
|
|
11
|
+
import MeterForm, { type TokenConfig } from '../../../../components/meter/form';
|
|
11
12
|
import DrawerForm from '../../../../components/drawer-form';
|
|
12
13
|
|
|
14
|
+
type TMeterForm = TMeter & {
|
|
15
|
+
mode?: 'OffChain' | 'OnChain';
|
|
16
|
+
tokenFactoryAddress?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
13
19
|
export default function MeterCreate() {
|
|
14
20
|
const { t } = useLocaleContext();
|
|
15
21
|
const { refresh } = usePaymentContext();
|
|
16
|
-
const
|
|
22
|
+
const [tokenConfig, setTokenConfig] = useLocalStorageState<TokenConfig>('payment_kit_token_config', {
|
|
23
|
+
defaultValue: { name: '', symbol: '', tokenFactoryAddress: '' },
|
|
24
|
+
});
|
|
25
|
+
const methods = useForm<TMeterForm>({
|
|
17
26
|
defaultValues: {
|
|
18
27
|
name: '',
|
|
19
28
|
event_name: '',
|
|
@@ -21,22 +30,38 @@ export default function MeterCreate() {
|
|
|
21
30
|
unit: '',
|
|
22
31
|
description: '',
|
|
23
32
|
metadata: {},
|
|
33
|
+
mode: 'OffChain',
|
|
24
34
|
},
|
|
25
35
|
});
|
|
26
36
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const onSubmit = async (data: TMeter) => {
|
|
37
|
+
const onSubmit = async (data: TMeterForm) => {
|
|
30
38
|
try {
|
|
31
|
-
|
|
39
|
+
const submitData: any = { ...data };
|
|
40
|
+
delete submitData.mode;
|
|
41
|
+
|
|
42
|
+
if (data.mode === 'OnChain') {
|
|
43
|
+
let tokenFactoryAddress = tokenConfig?.tokenFactoryAddress;
|
|
44
|
+
// Create new token if no existing token selected
|
|
45
|
+
if (!tokenFactoryAddress && tokenConfig?.symbol) {
|
|
46
|
+
const { data: factory } = await api.post('/api/credit-tokens', {
|
|
47
|
+
name: tokenConfig.name,
|
|
48
|
+
symbol: tokenConfig.symbol,
|
|
49
|
+
});
|
|
50
|
+
tokenFactoryAddress = factory.address;
|
|
51
|
+
}
|
|
52
|
+
if (tokenFactoryAddress) {
|
|
53
|
+
submitData.token = { tokenFactoryAddress };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await api.post('/api/meters', submitData);
|
|
32
58
|
Toast.success(t('admin.meter.saved'));
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
setTokenConfig({ name: '', symbol: '', tokenFactoryAddress: '' });
|
|
60
|
+
methods.reset();
|
|
35
61
|
dispatch('meter.created');
|
|
36
62
|
dispatch('drawer.submitted');
|
|
37
63
|
refresh(true);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
console.error(err);
|
|
64
|
+
} catch (err: any) {
|
|
40
65
|
Toast.error(formatError(err));
|
|
41
66
|
}
|
|
42
67
|
};
|
|
@@ -45,15 +70,19 @@ export default function MeterCreate() {
|
|
|
45
70
|
<DrawerForm
|
|
46
71
|
icon={<AddOutlined />}
|
|
47
72
|
text={t('admin.meter.add')}
|
|
48
|
-
onClose={
|
|
73
|
+
onClose={methods.clearErrors}
|
|
49
74
|
width={640}
|
|
50
75
|
addons={
|
|
51
|
-
<Button
|
|
76
|
+
<Button
|
|
77
|
+
variant="contained"
|
|
78
|
+
size="small"
|
|
79
|
+
disabled={methods.formState.isSubmitting}
|
|
80
|
+
onClick={methods.handleSubmit(onSubmit as any)}>
|
|
52
81
|
{t('admin.meter.save')}
|
|
53
82
|
</Button>
|
|
54
83
|
}>
|
|
55
84
|
<FormProvider {...methods}>
|
|
56
|
-
<MeterForm />
|
|
85
|
+
<MeterForm tokenConfig={tokenConfig!} onTokenConfigChange={setTokenConfig} />
|
|
57
86
|
</FormProvider>
|
|
58
87
|
</DrawerForm>
|
|
59
88
|
);
|
|
@@ -20,7 +20,7 @@ import SectionHeader from '../../../../components/section/header';
|
|
|
20
20
|
import { goBackOrFallback } from '../../../../libs/util';
|
|
21
21
|
import MeterEventsList from '../../../../components/meter/events-list';
|
|
22
22
|
import MeterProducts from '../../../../components/meter/products';
|
|
23
|
-
import MeterForm from '../../../../components/meter/form';
|
|
23
|
+
import MeterForm, { type TokenConfig } from '../../../../components/meter/form';
|
|
24
24
|
import DrawerForm from '../../../../components/drawer-form';
|
|
25
25
|
|
|
26
26
|
const getMeter = (id: string): Promise<TMeter & { paymentCurrency: TPaymentCurrency }> => {
|
|
@@ -58,9 +58,14 @@ export default function MeterDetail(props: { id: string }) {
|
|
|
58
58
|
product: false,
|
|
59
59
|
creditProduct: false,
|
|
60
60
|
},
|
|
61
|
+
tokenConfig: { name: '', symbol: '', tokenFactoryAddress: '' } as TokenConfig,
|
|
61
62
|
});
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
type TMeterForm = TMeter & {
|
|
65
|
+
mode?: 'OffChain' | 'OnChain';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const methods = useForm<TMeterForm>({
|
|
64
69
|
defaultValues: {
|
|
65
70
|
name: '',
|
|
66
71
|
event_name: '',
|
|
@@ -68,6 +73,7 @@ export default function MeterDetail(props: { id: string }) {
|
|
|
68
73
|
unit: '',
|
|
69
74
|
description: '',
|
|
70
75
|
metadata: {},
|
|
76
|
+
mode: 'OffChain',
|
|
71
77
|
},
|
|
72
78
|
});
|
|
73
79
|
|
|
@@ -108,6 +114,7 @@ export default function MeterDetail(props: { id: string }) {
|
|
|
108
114
|
};
|
|
109
115
|
|
|
110
116
|
const handleEditMeter = () => {
|
|
117
|
+
const hasTokenConfig = !!data.paymentCurrency?.token_config;
|
|
111
118
|
// 设置表单默认值
|
|
112
119
|
methods.reset({
|
|
113
120
|
name: data.name,
|
|
@@ -116,14 +123,53 @@ export default function MeterDetail(props: { id: string }) {
|
|
|
116
123
|
unit: data.unit,
|
|
117
124
|
description: data.description || '',
|
|
118
125
|
metadata: data.metadata || {},
|
|
126
|
+
mode: hasTokenConfig ? 'OnChain' : 'OffChain',
|
|
119
127
|
});
|
|
120
|
-
setState((prev) => ({
|
|
128
|
+
setState((prev) => ({
|
|
129
|
+
editing: { ...prev.editing, meter: true },
|
|
130
|
+
tokenConfig: { name: '', symbol: '', tokenFactoryAddress: '' },
|
|
131
|
+
}));
|
|
121
132
|
};
|
|
122
133
|
|
|
123
|
-
const onUpdateMeter = async (formData:
|
|
134
|
+
const onUpdateMeter = async (formData: TMeterForm) => {
|
|
124
135
|
try {
|
|
125
136
|
setState((prev) => ({ loading: { ...prev.loading, meter: true } }));
|
|
137
|
+
|
|
138
|
+
const currencyId = data.paymentCurrency?.id;
|
|
139
|
+
const hasTokenConfig = !!data.paymentCurrency?.token_config;
|
|
140
|
+
|
|
141
|
+
// If enabling on-chain and no token_config yet
|
|
142
|
+
if (currencyId && !hasTokenConfig && formData.mode === 'OnChain') {
|
|
143
|
+
let { tokenFactoryAddress } = state.tokenConfig;
|
|
144
|
+
|
|
145
|
+
// Create new token if not using existing
|
|
146
|
+
if (!tokenFactoryAddress && state.tokenConfig.symbol) {
|
|
147
|
+
const { data: factory } = await api.post('/api/credit-tokens', {
|
|
148
|
+
name: state.tokenConfig.name,
|
|
149
|
+
symbol: state.tokenConfig.symbol,
|
|
150
|
+
});
|
|
151
|
+
tokenFactoryAddress = factory.address;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Update currency with token_config
|
|
155
|
+
if (tokenFactoryAddress) {
|
|
156
|
+
await api.put(`/api/payment-currencies/${currencyId}/token-config`, {
|
|
157
|
+
token_factory_address: tokenFactoryAddress,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If disabling on-chain and has token_config, remove it
|
|
163
|
+
if (currencyId && hasTokenConfig && formData.mode === 'OffChain') {
|
|
164
|
+
await api.delete(`/api/payment-currencies/${currencyId}/token-config`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Clean up frontend-only fields before sending
|
|
168
|
+
delete formData.mode;
|
|
169
|
+
|
|
170
|
+
// Update meter basic info
|
|
126
171
|
await api.put(`/api/meters/${props.id}`, formData);
|
|
172
|
+
|
|
127
173
|
Toast.success(t('admin.meter.saved'));
|
|
128
174
|
setState((prev) => ({ editing: { ...prev.editing, meter: false } }));
|
|
129
175
|
runAsync();
|
|
@@ -442,7 +488,12 @@ export default function MeterDetail(props: { id: string }) {
|
|
|
442
488
|
</Button>
|
|
443
489
|
}>
|
|
444
490
|
<FormProvider {...methods}>
|
|
445
|
-
<MeterForm
|
|
491
|
+
<MeterForm
|
|
492
|
+
mode="edit"
|
|
493
|
+
paymentCurrency={data.paymentCurrency}
|
|
494
|
+
tokenConfig={state.tokenConfig}
|
|
495
|
+
onTokenConfigChange={(next) => setState({ tokenConfig: next })}
|
|
496
|
+
/>
|
|
446
497
|
</FormProvider>
|
|
447
498
|
</DrawerForm>
|
|
448
499
|
)}
|