402-trinity-gaming 0.1.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/INTEGRATION.md +188 -0
- package/LICENSE +110 -0
- package/README.md +105 -0
- package/dist/batch-manager.d.ts +201 -0
- package/dist/batch-manager.js +291 -0
- package/dist/batch-manager.min.js +1 -0
- package/dist/budget-file.d.ts +106 -0
- package/dist/budget-file.js +270 -0
- package/dist/budget-file.min.js +1 -0
- package/dist/evm-tx.d.ts +55 -0
- package/dist/evm-tx.js +195 -0
- package/dist/evm-tx.min.js +1 -0
- package/dist/proceeds-fee.d.ts +87 -0
- package/dist/proceeds-fee.js +158 -0
- package/dist/proceeds-fee.min.js +1 -0
- package/dist/seller.d.ts +121 -0
- package/dist/seller.js +136 -0
- package/dist/seller.min.js +1 -0
- package/dist/signer.d.ts +64 -0
- package/dist/signer.js +61 -0
- package/dist/signer.min.js +1 -0
- package/dist/storefront.d.ts +143 -0
- package/dist/storefront.js +173 -0
- package/dist/storefront.min.js +1 -0
- package/dist/x402.d.ts +391 -0
- package/dist/x402.js +930 -0
- package/dist/x402.min.js +1 -0
- package/package.json +120 -0
- package/src/batch-manager.ts +351 -0
- package/src/budget-file.ts +314 -0
- package/src/evm-tx.ts +244 -0
- package/src/proceeds-fee.ts +202 -0
- package/src/seller.ts +252 -0
- package/src/signer.ts +129 -0
- package/src/storefront.ts +286 -0
- package/src/x402.ts +1255 -0
package/INTEGRATION.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# ⥠402-Trinity-Gaming â Integration Brief
|
|
2
|
+
|
|
3
|
+
**A white-label microtransaction engine for game storefronts. The player holds their own wallet.**
|
|
4
|
+
|
|
5
|
+
Players buy cosmetics, battle passes and timer skips without ever leaving your game. No
|
|
6
|
+
overlay, no browser tab, no checkout screen wearing someone else's brand. Your button, your
|
|
7
|
+
art, your unlock animation.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## đ ī¸ Step 1 â Studio setup (done once)
|
|
12
|
+
|
|
13
|
+
**Install** into your existing game backend:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install 402-trinity-gaming
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Configure** your catalog and your treasury:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const store = createStorefront({
|
|
23
|
+
payTo: '0xYourStudioTreasury',
|
|
24
|
+
network: 'base',
|
|
25
|
+
facilitator: 'https://...',
|
|
26
|
+
nonceStore,
|
|
27
|
+
catalog: {
|
|
28
|
+
vanguard_skin_01: '1500000', // $1.50
|
|
29
|
+
season_pass_04: '9990000', // $9.99
|
|
30
|
+
},
|
|
31
|
+
surcharge: { proceedsKey },
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Hook up your UI.** Your artists design the store exactly how they want. When a player taps
|
|
36
|
+
*Purchase*, your button calls your server, and two events come back â one when the purchase is
|
|
37
|
+
accepted, one when the money has moved:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
store.on('settled', e => grantItem(e.playerId, e.itemId));
|
|
41
|
+
store.on('declined', e => showRefusal(e.playerId, e.code));
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That is the whole integration surface. Nothing renders, nothing takes over input, and nothing
|
|
45
|
+
writes to your console â the build fails if it does.
|
|
46
|
+
|
|
47
|
+
> Prices live in your server's catalog, never in the client call. A client that names its own
|
|
48
|
+
> price is a client that sets its own price.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## đšī¸ Step 2 â Player wallet (done once per player)
|
|
53
|
+
|
|
54
|
+
When a player creates an account, your client generates a wallet on their device.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const { privateKey, address } = createPlayerWallet();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
You store the address. **They** keep the key â you never hold it, and you cannot spend from
|
|
61
|
+
their wallet. The player is the real buyer, paying you directly.
|
|
62
|
+
|
|
63
|
+
> **If you use tabs** (see below), note that a partly-spent tab *is* a balance you hold: the
|
|
64
|
+
> player's cash is yours, and what they have left is credit in your game. That is closed-loop
|
|
65
|
+
> â spendable only on your items, never withdrawable â so it is the same shape as any in-game
|
|
66
|
+
> currency, not a deposit account. Worth raising with your counsel all the same.
|
|
67
|
+
|
|
68
|
+
> **Yours to build today:** the encryption, unlock and backup flow. We hand you the key; where
|
|
69
|
+
> it sleeps is your product decision. There is no password reset â give players a recovery
|
|
70
|
+
> phrase or an encrypted backup at setup.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## đŗ Step 3 â Funding the wallet
|
|
75
|
+
|
|
76
|
+
It is an ordinary Base address, so there are two doors and you can open either.
|
|
77
|
+
|
|
78
|
+
**Players who hold crypto** send USDC straight to it from any exchange or wallet.
|
|
79
|
+
|
|
80
|
+
**Everyone else** goes through a card on-ramp you drop into your launcher or account page.
|
|
81
|
+
They pay with a card, USDC lands in their wallet.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## âī¸ Step 4 â The checkout loop
|
|
86
|
+
|
|
87
|
+
A player taps *Buy* on a $5.00 weapon skin, mid-lobby.
|
|
88
|
+
|
|
89
|
+
**On their machine**, the client signs one authorization â about **3 milliseconds**,
|
|
90
|
+
comfortably inside a frame, touching no network. Only the signature leaves the device.
|
|
91
|
+
|
|
92
|
+
**On your server**, that signature is verified and settled through your facilitator.
|
|
93
|
+
|
|
94
|
+
**Back in the game**, `accepted` fires the moment the signature checks out, and `settled` when
|
|
95
|
+
the transfer confirms on Base a few seconds later.
|
|
96
|
+
|
|
97
|
+
**Design this bit deliberately.** Settlement is a real transfer on a real chain â seconds, not
|
|
98
|
+
milliseconds. Want the skin to appear the instant they tap? Grant on `accepted` and reconcile
|
|
99
|
+
on `settled`. Rather never take something back? Wait for `settled`. One line either way.
|
|
100
|
+
|
|
101
|
+
**The player is debited exactly $5.00.** The price on the label is the price they pay.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## đ Tabs â for games that charge constantly
|
|
106
|
+
|
|
107
|
+
Survival, MMO and sandbox economies bill in fractions of a cent: a timer skip, a stack of ore,
|
|
108
|
+
a repair. Settling each one on-chain costs more in gas than the action costs the player.
|
|
109
|
+
|
|
110
|
+
So the player **loads a tab** â one signature, one transfer â and every action after that is a
|
|
111
|
+
deduction from that credit. No signature, no chain, no gas, nothing to wait for.
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
const tabs = createBatchManager({
|
|
115
|
+
...sameConfigAsAbove,
|
|
116
|
+
tabs: { session: '2000000' }, // $2.00 of credit
|
|
117
|
+
ledger, // durable - see below
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await tabs.open({ tabId: 'session', playerId, playerAddress, authorization, signature });
|
|
121
|
+
|
|
122
|
+
// then, on any gameplay path:
|
|
123
|
+
await tabs.spend({ playerId, actionId: 'skip:furnace:8412', amount: '20000' });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Why it is worth doing:** a hundred separate $0.02 payments cost roughly $0.13 in gas. One
|
|
127
|
+
$2.00 tab costs about $0.0013. Same money to you.
|
|
128
|
+
|
|
129
|
+
**`actionId` makes spending idempotent.** A client that retries after a dropped connection is
|
|
130
|
+
charged once; the second call reports `duplicate: true` so you still grant the item.
|
|
131
|
+
|
|
132
|
+
**Your ledger must be durable.** In memory, every player's remaining credit dies with the
|
|
133
|
+
process â they paid for something your server no longer remembers. Use the file store for a
|
|
134
|
+
single instance, or your own database adapter for more than one:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import { createFileLedgerStore } from '402-trinity-gaming/budget-file';
|
|
138
|
+
const ledger = createFileLedgerStore('./tabs.json');
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**If you run more than one game server**, implement `ledger.update()` and hold a row lock
|
|
142
|
+
across the read and the write. Without it, two servers can read the same balance and both
|
|
143
|
+
approve a spend it could only cover once.
|
|
144
|
+
|
|
145
|
+
**A tab is a balance you hold.** The player's cash is yours the moment the tab opens; what
|
|
146
|
+
they have left is credit in your game.
|
|
147
|
+
|
|
148
|
+
**Giving it back:**
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
await tabs.refund({ playerId }); // everything unspent
|
|
152
|
+
await tabs.refund({ playerId, amount: '500000' }); // or part of it
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Your treasury signs, the facilitator submits, the USDC lands back in the player's wallet â you
|
|
156
|
+
need no gas. The credit is deducted before the transfer is attempted, so it cannot be spent
|
|
157
|
+
while the refund is in flight, and it is **put back if the transfer fails**. Requires
|
|
158
|
+
`surcharge.proceedsKey`, since that is the key for the wallet holding the money.
|
|
159
|
+
|
|
160
|
+
The 0.1% taken when the tab opened is not reversed â it was charged on a sale that happened.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## đ§ž When something is refused
|
|
165
|
+
|
|
166
|
+
Every refusal comes back with a code you can switch on â item not for sale, bad signature,
|
|
167
|
+
authorization already spent, or settlement failed on the server's side.
|
|
168
|
+
|
|
169
|
+
Each also carries **`retryable`**, and it is the field that stops you charging a player twice.
|
|
170
|
+
When settlement fails on our side the payment was valid, so the *same* authorization must go
|
|
171
|
+
out again â minting a fresh one risks paying twice if the first lands late. When a signature
|
|
172
|
+
is rejected or already spent, it will never work. We make that call so your code doesn't have
|
|
173
|
+
to.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## đĻ What you get today
|
|
178
|
+
|
|
179
|
+
**Available now:** the TypeScript backend library and client signer. Base mainnet, USDC.
|
|
180
|
+
|
|
181
|
+
**Coming:** native Unity (C#) and Unreal (C++) packages â the signer is a single function and
|
|
182
|
+
porting it is next. Key encryption and the card on-ramp are yours for now.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
Business Source License 1.1. Source-available; converts to MIT on 2029-08-25.
|
|
187
|
+
|
|
188
|
+
Merchant proceeds are settled net of a 0.1% network fee.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
|
4
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
5
|
+
|
|
6
|
+
-----------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
|
|
10
|
+
Licensor: x402 trinity
|
|
11
|
+
|
|
12
|
+
Licensed Work: x402-trinity
|
|
13
|
+
The Licensed Work is (c) 2026 x402 trinity
|
|
14
|
+
|
|
15
|
+
Additional Use Grant: You may make use of the Licensed Work, provided that
|
|
16
|
+
you do not offer the Licensed Work to third parties as
|
|
17
|
+
a hosted or managed service that provides users with
|
|
18
|
+
access to any substantial set of its features or
|
|
19
|
+
functionality.
|
|
20
|
+
|
|
21
|
+
Change Date: 2029-08-25
|
|
22
|
+
|
|
23
|
+
Change License: MIT
|
|
24
|
+
|
|
25
|
+
For information about alternative licensing arrangements for the Licensed
|
|
26
|
+
Work, please contact the Licensor.
|
|
27
|
+
|
|
28
|
+
-----------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
Notice
|
|
31
|
+
|
|
32
|
+
Business Source License 1.1
|
|
33
|
+
|
|
34
|
+
Terms
|
|
35
|
+
|
|
36
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
37
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
38
|
+
Licensor may make an Additional Use Grant, above, permitting limited
|
|
39
|
+
production use.
|
|
40
|
+
|
|
41
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
42
|
+
available distribution of a specific version of the Licensed Work under this
|
|
43
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
44
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
45
|
+
above terminate.
|
|
46
|
+
|
|
47
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
48
|
+
currently in effect as described in this License, you must purchase a
|
|
49
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
50
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
51
|
+
|
|
52
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
53
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
54
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
55
|
+
for each version of the Licensed Work released by Licensor.
|
|
56
|
+
|
|
57
|
+
You must conspicuously display this License on each original or modified copy
|
|
58
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
59
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
60
|
+
License apply to your use of that work.
|
|
61
|
+
|
|
62
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
63
|
+
terminate your rights under this License for the current and all other
|
|
64
|
+
versions of the Licensed Work.
|
|
65
|
+
|
|
66
|
+
This License does not grant you any right in any trademark or logo of
|
|
67
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
68
|
+
Licensor as expressly required by this License).
|
|
69
|
+
|
|
70
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
71
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
72
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
73
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
74
|
+
TITLE.
|
|
75
|
+
|
|
76
|
+
MariaDB hereby grants you permission to use this License's text to license
|
|
77
|
+
your works, and to refer to it using the trademark "Business Source License",
|
|
78
|
+
as long as you comply with the Covenants of Licensor below.
|
|
79
|
+
|
|
80
|
+
Covenants of Licensor
|
|
81
|
+
|
|
82
|
+
In consideration of the right to use this License's text and the "Business
|
|
83
|
+
Source License" name and trademark, Licensor covenants to MariaDB, and to all
|
|
84
|
+
other recipients of the licensed work to be provided by Licensor:
|
|
85
|
+
|
|
86
|
+
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
|
87
|
+
or a license that is compatible with GPL Version 2.0 or a later version,
|
|
88
|
+
where "compatible" means that software provided under the Change License
|
|
89
|
+
can be included in a program with software provided under GPL Version 2.0
|
|
90
|
+
or a later version. Licensor may specify additional Change Licenses
|
|
91
|
+
without limitation.
|
|
92
|
+
|
|
93
|
+
2. To either: (a) specify an additional grant of rights to use that does not
|
|
94
|
+
impose any additional restriction on the right granted in this License, as
|
|
95
|
+
the Additional Use Grant; or (b) insert the text "None".
|
|
96
|
+
|
|
97
|
+
3. To specify a Change Date.
|
|
98
|
+
|
|
99
|
+
4. Not to modify this License in any other way.
|
|
100
|
+
|
|
101
|
+
-----------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
THIS SOFTWARE MOVES MONEY. Additional notice:
|
|
104
|
+
|
|
105
|
+
This software constructs and signs cryptographic payment authorizations. You
|
|
106
|
+
are solely responsible for the funds in any wallet you configure it with, for
|
|
107
|
+
the spending limits you set, and for the behaviour of any autonomous agent you
|
|
108
|
+
connect to it. Set `maxAmountPerRequest` and `totalBudget`, use a dedicated
|
|
109
|
+
wallet, and read the security notes in the README before using it with real
|
|
110
|
+
assets.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# 402-trinity-gaming
|
|
2
|
+
|
|
3
|
+
**A payment backend for game storefronts that your players never see.**
|
|
4
|
+
|
|
5
|
+
No UI. No overlay. No browser handoff. No console output. Your button, your art, your unlock
|
|
6
|
+
animation â this handles the money and gets out of the way.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install 402-trinity-gaming
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## How it fits
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Your storefront UI âââļ your backend âââļ chain
|
|
16
|
+
(Unreal/Unity) (this package)
|
|
17
|
+
Ⲡâ
|
|
18
|
+
âââââââ events ââââââââââ
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The player's client signs one authorization with their own key. Your server does everything
|
|
22
|
+
else. **You never hold the player's key**, and for direct purchases you hold no balance
|
|
23
|
+
either. (Tabs are the exception - see `INTEGRATION.md`.)
|
|
24
|
+
|
|
25
|
+
## Use
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { createStorefront } from '402-trinity-gaming/storefront';
|
|
29
|
+
|
|
30
|
+
const store = createStorefront({
|
|
31
|
+
payTo: '0xYourStudioWallet',
|
|
32
|
+
network: 'base',
|
|
33
|
+
facilitator: 'https://your-facilitator.example', // REQUIRED - no default
|
|
34
|
+
nonceStore, // REQUIRED - must survive restarts
|
|
35
|
+
catalog: {
|
|
36
|
+
vanguard_skin_01: '1500000', // atomic units: 1.50 USDC (6 decimals)
|
|
37
|
+
season_pass_04: '9990000', // 9.99
|
|
38
|
+
},
|
|
39
|
+
surcharge: { proceedsKey }, // the key for payTo
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
store.on('settled', e => grantItem(e.playerId, e.itemId));
|
|
43
|
+
store.on('declined', e => showRefusal(e.playerId, e.code));
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Serve the quote, take the signature, redeem it:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// 1. the client asks what it must sign
|
|
50
|
+
const quote = store.quote('vanguard_skin_01');
|
|
51
|
+
|
|
52
|
+
// 2. the client signs it and posts back { authorization, signature }
|
|
53
|
+
const result = await store.purchase({
|
|
54
|
+
itemId: 'vanguard_skin_01',
|
|
55
|
+
playerId: 'player-8823',
|
|
56
|
+
playerAddress: '0xPlayerWallet',
|
|
57
|
+
authorization,
|
|
58
|
+
signature,
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Events
|
|
63
|
+
|
|
64
|
+
| event | when | what to do |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `accepted` | the request is well formed, the item is real, settlement is underway | grant optimistically if you want the item to appear instantly |
|
|
67
|
+
| `settled` | the money has moved, with an on-chain transaction hash | grant, or reconcile an optimistic grant |
|
|
68
|
+
| `declined` | it was refused | read `code`, not `message` |
|
|
69
|
+
|
|
70
|
+
Decline codes are `unknown_item`, `already_used`, `rejected`, `settlement_failed` and
|
|
71
|
+
`malformed`. Each carries `retryable` â **true** means the same authorization may be presented
|
|
72
|
+
again unchanged, **false** means mint a fresh one. Re-sending when `retryable` is false risks
|
|
73
|
+
paying twice.
|
|
74
|
+
|
|
75
|
+
## Prices are strings
|
|
76
|
+
|
|
77
|
+
Atomic units of the asset, as a decimal string. USDC has 6 decimals, so `1.50` is
|
|
78
|
+
`'1500000'`. A float cannot represent money exactly, and this value goes inside a signature.
|
|
79
|
+
|
|
80
|
+
## Nothing is printed
|
|
81
|
+
|
|
82
|
+
This library never writes to `stdout` or `stderr` â that is enforced by its build, not by
|
|
83
|
+
convention. Diagnostics reach you through `onDiagnostic`, and the fee disclosure is exported
|
|
84
|
+
as `NOTICE` for you to place wherever disclosure belongs in your product.
|
|
85
|
+
|
|
86
|
+
## Scope
|
|
87
|
+
|
|
88
|
+
**Base mainnet + USDC.** Other EVM chains work by passing `customChains`. Check any entry
|
|
89
|
+
against the deployed contract first: call `DOMAIN_SEPARATOR()` and confirm it matches what
|
|
90
|
+
this library computes. A wrong `name` or `version` produces a signature that looks valid and
|
|
91
|
+
the contract rejects.
|
|
92
|
+
|
|
93
|
+
## Before you use it with real money
|
|
94
|
+
|
|
95
|
+
This software signs payment authorizations. You are responsible for the funds in any wallet
|
|
96
|
+
you configure it with and for the limits you set. `nonceStore` must be durable â an in-memory
|
|
97
|
+
replay guard forgets every settled payment on restart, which means selling the same item twice
|
|
98
|
+
for free.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
Business Source License 1.1. Source-available; converts to MIT on 2029-08-25. See
|
|
103
|
+
[LICENSE](LICENSE).
|
|
104
|
+
|
|
105
|
+
Network fee: 0.1% of merchant proceeds and 1 cent every 100 sales.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OFF-CHAIN MICRO-ACTION LEDGER.
|
|
3
|
+
*
|
|
4
|
+
* Survival, MMO and sandbox economies charge constantly and in fractions of a cent - a timer
|
|
5
|
+
* skip, a stack of ore, a repair. Settling each one on-chain costs more in gas than the
|
|
6
|
+
* action costs the player, so nothing here touches the chain per action.
|
|
7
|
+
*
|
|
8
|
+
* Instead the player opens a TAB: one signed authorization, settled once, credited to a local
|
|
9
|
+
* ledger. Every action after that is a synchronous deduction - no signature, no network, no
|
|
10
|
+
* frame cost. When the tab runs low the player tops up with another single signature.
|
|
11
|
+
*
|
|
12
|
+
* const tab = await tabs.open({ playerId, playerAddress, authorization, signature });
|
|
13
|
+
* tabs.spend({ playerId, actionId: 'skip:furnace:8412', amount: '20000' }); // instant
|
|
14
|
+
*
|
|
15
|
+
* WHY A TAB AND NOT A HUNDRED CACHED SIGNATURES. An EIP-3009 authorization is redeemed by its
|
|
16
|
+
* own contract call with its own nonce; a hundred of them cannot be summed into one transfer.
|
|
17
|
+
* Caching signatures and flushing them together still costs a hundred settlements - about
|
|
18
|
+
* 6.4% of a $2.00 batch at real Base gas, against 0.06% for a single one. The saving comes
|
|
19
|
+
* from one signature covering the batch, not from when the signatures are sent.
|
|
20
|
+
*
|
|
21
|
+
* WHY PREPAID AND NOT POSTPAID. Charging at the END of a hundred actions means granting
|
|
22
|
+
* ninety-nine of them on credit. A player who closes the game keeps them, and a farm of bots
|
|
23
|
+
* does it deliberately. Money in hand first removes the question.
|
|
24
|
+
*/
|
|
25
|
+
import { type StorefrontConfig, type PurchaseDeclined } from './storefront.js';
|
|
26
|
+
import type { Authorization } from './x402.js';
|
|
27
|
+
export interface TabConfig extends Omit<StorefrontConfig, 'catalog'> {
|
|
28
|
+
/**
|
|
29
|
+
* Tab sizes a player may open, in atomic units. Named like a catalog because that is what
|
|
30
|
+
* they are - a $2.00 tab is a $2.00 purchase that happens to be spent gradually.
|
|
31
|
+
*/
|
|
32
|
+
tabs: Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* Durable ledger. WITHOUT ONE, EVERY PLAYER'S REMAINING BALANCE IS LOST ON RESTART - they
|
|
35
|
+
* paid for credit the process no longer remembers. In memory is for local development only.
|
|
36
|
+
*/
|
|
37
|
+
ledger?: LedgerStore;
|
|
38
|
+
/** Warn when a tab drops below this fraction of its opening size. Default 0.15. */
|
|
39
|
+
lowWaterMark?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface LedgerStore {
|
|
42
|
+
get: (playerId: string) => Promise<TabState | null>;
|
|
43
|
+
set: (playerId: string, s: TabState) => Promise<void>;
|
|
44
|
+
/** Read-modify-write under a lock. Without it two game servers double-spend one tab. */
|
|
45
|
+
update?: (playerId: string, fn: (cur: TabState | null) => TabState) => Promise<TabState>;
|
|
46
|
+
}
|
|
47
|
+
export interface TabState {
|
|
48
|
+
playerAddress: string;
|
|
49
|
+
/** Atomic units still available. */
|
|
50
|
+
remaining: string;
|
|
51
|
+
/** What the tab was opened for, for reporting. */
|
|
52
|
+
opened: string;
|
|
53
|
+
/**
|
|
54
|
+
* Action ids already charged, with when. Kept BY AGE rather than by count: a retry happens
|
|
55
|
+
* within seconds, so a day is generous - and a cap by count means a long-lived tab silently
|
|
56
|
+
* forgets an old id and charges for it a second time.
|
|
57
|
+
*/
|
|
58
|
+
spent: Array<{
|
|
59
|
+
id: string;
|
|
60
|
+
at: number;
|
|
61
|
+
}>;
|
|
62
|
+
updatedAt: string;
|
|
63
|
+
}
|
|
64
|
+
export type SpendResult = {
|
|
65
|
+
ok: true;
|
|
66
|
+
remaining: string;
|
|
67
|
+
charged: string;
|
|
68
|
+
duplicate: boolean;
|
|
69
|
+
} | {
|
|
70
|
+
ok: false;
|
|
71
|
+
code: 'no_tab' | 'insufficient' | 'invalid_amount';
|
|
72
|
+
remaining: string;
|
|
73
|
+
message: string;
|
|
74
|
+
};
|
|
75
|
+
export type RefundResult = {
|
|
76
|
+
ok: true;
|
|
77
|
+
refunded: string;
|
|
78
|
+
remaining: string;
|
|
79
|
+
transaction: string;
|
|
80
|
+
} | {
|
|
81
|
+
ok: false;
|
|
82
|
+
code: 'no_tab' | 'nothing_to_refund' | 'too_much' | 'no_key' | 'failed';
|
|
83
|
+
remaining: string;
|
|
84
|
+
message: string;
|
|
85
|
+
};
|
|
86
|
+
interface Events {
|
|
87
|
+
opened: {
|
|
88
|
+
playerId: string;
|
|
89
|
+
playerAddress: string;
|
|
90
|
+
amount: string;
|
|
91
|
+
transaction: string;
|
|
92
|
+
};
|
|
93
|
+
refunded: {
|
|
94
|
+
playerId: string;
|
|
95
|
+
playerAddress: string;
|
|
96
|
+
amount: string;
|
|
97
|
+
transaction: string;
|
|
98
|
+
};
|
|
99
|
+
spent: {
|
|
100
|
+
playerId: string;
|
|
101
|
+
actionId: string;
|
|
102
|
+
amount: string;
|
|
103
|
+
remaining: string;
|
|
104
|
+
};
|
|
105
|
+
low: {
|
|
106
|
+
playerId: string;
|
|
107
|
+
remaining: string;
|
|
108
|
+
opened: string;
|
|
109
|
+
};
|
|
110
|
+
exhausted: {
|
|
111
|
+
playerId: string;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export declare function createBatchManager(cfg: TabConfig): {
|
|
115
|
+
/** Tab sizes on offer. */
|
|
116
|
+
readonly sizes: string[];
|
|
117
|
+
/** What the client signs to open tab `tabId`. */
|
|
118
|
+
quote(tabId: string): {
|
|
119
|
+
scheme: string;
|
|
120
|
+
network: string;
|
|
121
|
+
amount: string;
|
|
122
|
+
asset: string;
|
|
123
|
+
payTo: string;
|
|
124
|
+
maxTimeoutSeconds: number;
|
|
125
|
+
extra: {
|
|
126
|
+
name: string;
|
|
127
|
+
version: string;
|
|
128
|
+
};
|
|
129
|
+
itemId: string;
|
|
130
|
+
} | null;
|
|
131
|
+
on<K extends keyof Events>(n: K, h: (e: Events[K]) => void): () => void;
|
|
132
|
+
/**
|
|
133
|
+
* Open or top up a tab. This is the ONLY on-chain step - one settlement covering every
|
|
134
|
+
* action the player takes until the balance runs out.
|
|
135
|
+
*
|
|
136
|
+
* Credit is added only after the transfer settles. A declined payment adds nothing.
|
|
137
|
+
*/
|
|
138
|
+
open(req: {
|
|
139
|
+
tabId: string;
|
|
140
|
+
playerId: string;
|
|
141
|
+
playerAddress: string;
|
|
142
|
+
authorization: Authorization;
|
|
143
|
+
signature: string;
|
|
144
|
+
}): Promise<{
|
|
145
|
+
ok: true;
|
|
146
|
+
remaining: string;
|
|
147
|
+
transaction: string;
|
|
148
|
+
} | {
|
|
149
|
+
ok: false;
|
|
150
|
+
declined: PurchaseDeclined;
|
|
151
|
+
}>;
|
|
152
|
+
/**
|
|
153
|
+
* Charge one micro-action. Synchronous in spirit - no signature, no chain, no network -
|
|
154
|
+
* so it is safe on a gameplay path.
|
|
155
|
+
*
|
|
156
|
+
* `actionId` makes it idempotent. A client that retries after a dropped response charges
|
|
157
|
+
* once, and the second call reports `duplicate: true` so you can grant without re-billing.
|
|
158
|
+
*/
|
|
159
|
+
spend(req: {
|
|
160
|
+
playerId: string;
|
|
161
|
+
actionId: string;
|
|
162
|
+
amount: string;
|
|
163
|
+
}): Promise<SpendResult>;
|
|
164
|
+
/**
|
|
165
|
+
* Return unspent credit to the player's wallet.
|
|
166
|
+
*
|
|
167
|
+
* The studio signs an authorization to the player and the facilitator submits it, so the
|
|
168
|
+
* studio needs no gas - the same shape as every other transfer here, just pointing the
|
|
169
|
+
* other way. Requires `surcharge.proceedsKey`, because that is the key for the wallet
|
|
170
|
+
* holding the money.
|
|
171
|
+
*
|
|
172
|
+
* ORDER MATTERS. The credit is deducted BEFORE the transfer is attempted, so it cannot be
|
|
173
|
+
* spent while the refund is in flight, and restored if the transfer fails. The opposite
|
|
174
|
+
* order lets a player spend the same money twice - once in-game and once on-chain.
|
|
175
|
+
*
|
|
176
|
+
* The 0.1% taken when the tab opened is NOT reversed. It was charged on a sale that did
|
|
177
|
+
* happen, and clawing it back out of the vault is not something this can do.
|
|
178
|
+
*/
|
|
179
|
+
refund(req: {
|
|
180
|
+
playerId: string;
|
|
181
|
+
amount?: string;
|
|
182
|
+
}): Promise<RefundResult>;
|
|
183
|
+
/** What the player has left. Read-only. */
|
|
184
|
+
balance(playerId: string): Promise<{
|
|
185
|
+
remaining: string;
|
|
186
|
+
opened: string;
|
|
187
|
+
} | null>;
|
|
188
|
+
/** The network fee taken from proceeds when a tab is opened. */
|
|
189
|
+
fee: {
|
|
190
|
+
readonly enabled: boolean;
|
|
191
|
+
stats: () => Promise<{
|
|
192
|
+
enabled: boolean;
|
|
193
|
+
salesSinceLastSweep: string;
|
|
194
|
+
accrued: string;
|
|
195
|
+
collected: string;
|
|
196
|
+
held: string;
|
|
197
|
+
lost: string;
|
|
198
|
+
}>;
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
export {};
|