@nexus-cross/pop 1.4.0-beta.1 → 1.4.0-beta.3
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/AGENTS.md +40 -49
- package/README.md +61 -53
- package/dist/adapters/index.d.ts +340 -874
- package/dist/adapters/index.js +1 -1
- package/dist/chunk-E3ZMMRDI.js +1 -0
- package/dist/{createSafeDrop-BSO9RQBD.d.ts → createSafeDrop-CayUX4w_.d.ts} +193 -126
- package/dist/index.d.ts +8 -43
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/infrastructure/openapi.json +836 -116
- package/dist/chunk-RS7SBR7A.js +0 -1
package/AGENTS.md
CHANGED
|
@@ -39,7 +39,7 @@ pnpm add @nexus-cross/pop viem # + react if using ./react
|
|
|
39
39
|
|
|
40
40
|
| What | Where it goes | Notes |
|
|
41
41
|
|---|---|---|
|
|
42
|
-
| SafeDrop contract address |
|
|
42
|
+
| SafeDrop contract address | selected by `ONE_POP_ENVIRONMENT` | explicit `contractAddress` remains an override |
|
|
43
43
|
| viem `publicClient` / `walletClient` / `chain` | same | walletClient = connected sponsor wallet |
|
|
44
44
|
| claim page base URL | `createSafeDropClient({ claimBaseUrl })` | the route that handles the claim link |
|
|
45
45
|
| SIWE JWT provider | `createSafeDropClient({ api: { getJwt } })` | **required** for deposit and all authed reads |
|
|
@@ -51,13 +51,14 @@ prefix matching the framework, Vite `VITE_` or Next.js `NEXT_PUBLIC_`:
|
|
|
51
51
|
|
|
52
52
|
| Variable | Purpose |
|
|
53
53
|
|---|---|
|
|
54
|
-
| `VITE_ONE_POP_ENVIRONMENT` / `NEXT_PUBLIC_ONE_POP_ENVIRONMENT` / `ONE_POP_ENVIRONMENT` | `dev` \| `stage` \| `production` (default `production`) → picks the
|
|
54
|
+
| `VITE_ONE_POP_ENVIRONMENT` / `NEXT_PUBLIC_ONE_POP_ENVIRONMENT` / `ONE_POP_ENVIRONMENT` | `dev` \| `stage` \| `production` (default `production`) → picks the API and contract deployment together |
|
|
55
55
|
| `VITE_ONE_POP_API_BASE_URL` / `NEXT_PUBLIC_ONE_POP_API_BASE_URL` | override the One Pop API base URL |
|
|
56
56
|
| `VITE_CROSS_AUTH_URL` / `NEXT_PUBLIC_CROSS_AUTH_URL` | override the cross-auth (SIWE) base URL |
|
|
57
57
|
|
|
58
58
|
Base URLs must be `https` (or `http://localhost` for local dev) or construction throws.
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
Dev and stage contract deployments are built in. Production contract addresses are not configured
|
|
60
|
+
yet, so production must pass `contractAddress` explicitly. Claim base URL and X client id remain app options.
|
|
61
|
+
The protocol fee is fixed at 5% (`ONE_POP_FEE_BPS = 500n`).
|
|
61
62
|
|
|
62
63
|
## 4. Wiring
|
|
63
64
|
|
|
@@ -75,12 +76,10 @@ const signature = await walletClient.signMessage({ account: address, message });
|
|
|
75
76
|
const { token } = await auth.siweToken(address, signature);
|
|
76
77
|
|
|
77
78
|
const safeDrop = createSafeDropClient({
|
|
78
|
-
contractAddress: SAFEDROP_ADDRESS,
|
|
79
79
|
publicClient,
|
|
80
80
|
walletClient,
|
|
81
81
|
chain,
|
|
82
82
|
claimBaseUrl: 'https://app.example/claim',
|
|
83
|
-
permit: false, // true only if BOTH the deployment and the token support EIP-2612
|
|
84
83
|
api: { getJwt: () => token },
|
|
85
84
|
});
|
|
86
85
|
```
|
|
@@ -94,7 +93,7 @@ The provider only carries the client through context — no connection logic liv
|
|
|
94
93
|
|
|
95
94
|
```ts
|
|
96
95
|
const envelopes = await safeDrop.listEnvelopes(); // optional card designs
|
|
97
|
-
const
|
|
96
|
+
const deposit = await safeDrop.deposit({
|
|
98
97
|
sender: address,
|
|
99
98
|
recipient: { provider: 'x', handle: 'theo_13303' },
|
|
100
99
|
token: ERC20_ADDRESS,
|
|
@@ -102,23 +101,25 @@ const { withdrawLink, claimAddress } = await safeDrop.deposit({
|
|
|
102
101
|
message: 'Happy birthday!', // optional, <= 140 runes
|
|
103
102
|
envelopeId: envelopes[0]?.id, // optional
|
|
104
103
|
});
|
|
104
|
+
if (!deposit.isMapped) {
|
|
105
|
+
location.assign(buildComposeUrl(buildDmText(deposit.withdrawLink), recipientId));
|
|
106
|
+
}
|
|
105
107
|
```
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
`location.assign(buildComposeUrl(buildDmText(withdrawLink), recipientId))`.
|
|
109
|
+
Mapped deposits have no claim link because the mapped recipient receives them directly.
|
|
109
110
|
|
|
110
111
|
### Claim (recipient, on the claim route)
|
|
111
112
|
|
|
112
113
|
```ts
|
|
113
|
-
const { sender, secret } = parseWithdrawLink(location.href);
|
|
114
|
+
const { sender, claimAddress, secret } = parseWithdrawLink(location.href);
|
|
114
115
|
const { oauth } = await xauth.complete(location.href); // X OAuth2 PKCE callback
|
|
115
|
-
const
|
|
116
|
-
|
|
116
|
+
const claim = await safeDrop.retrieveClaimKey({ claimAddress: claimAddress!, senderAddress: sender!, oauth });
|
|
117
|
+
if (claim.isMapped) throw new Error('Use the mapped withdrawal flow');
|
|
118
|
+
await safeDrop.withdraw({ recipient: myAddress, sponsor: sender!, claimAddress, secret: secret!, claimKey: claim.claimKey });
|
|
117
119
|
```
|
|
118
120
|
|
|
119
121
|
`retrieveClaimKey` is the point of no return — after it resolves, key custody is the client's.
|
|
120
|
-
|
|
121
|
-
`claimAddress` selector; multiple drops from the same sender are claimed one link at a time.
|
|
122
|
+
`claimAddress` selects the exact pending drop; OAuth proves entitlement to that drop's handle.
|
|
122
123
|
|
|
123
124
|
### Inbox / social surfaces
|
|
124
125
|
|
|
@@ -126,8 +127,8 @@ It returns **only the latest pending drop** for that `(sender, handle)` pair and
|
|
|
126
127
|
await safeDrop.connectX({ oauth }); // required once before listDrops
|
|
127
128
|
const conn = await safeDrop.getXConnection(); // null when not connected
|
|
128
129
|
const inbox = await safeDrop.listDrops(); // { items, count, totalAmount, hasClaimedBefore }
|
|
129
|
-
const top = await safeDrop.getLeaderboard({ token: ERC20_ADDRESS });
|
|
130
|
-
const page = await safeDrop.listHistories({
|
|
130
|
+
const top = await safeDrop.getLeaderboard({ token: ERC20_ADDRESS, page: 1, pageSize: 10 });
|
|
131
|
+
const page = await safeDrop.listHistories({ type: 'received', page: 1, pageSize: 20 });
|
|
131
132
|
await safeDrop.disconnectX();
|
|
132
133
|
```
|
|
133
134
|
|
|
@@ -137,7 +138,7 @@ batch-claim CTA on it.
|
|
|
137
138
|
### Refund
|
|
138
139
|
|
|
139
140
|
```ts
|
|
140
|
-
await safeDrop.refund({ claimAddress });
|
|
141
|
+
await safeDrop.refund({ claimAddress, sponsor: address });
|
|
141
142
|
```
|
|
142
143
|
|
|
143
144
|
### Batch claim — drain every drop for a handle in one flow
|
|
@@ -146,33 +147,25 @@ await safeDrop.refund({ claimAddress }); // sponsor only, after expiry
|
|
|
146
147
|
const { txHashes, recipient } = await safeDrop.batchClaim({
|
|
147
148
|
id: myHandle, // normalized internally
|
|
148
149
|
oauth, // X OAuth proof (handle ownership)
|
|
149
|
-
|
|
150
|
+
sponsor: sender,
|
|
151
|
+
claimAddress,
|
|
152
|
+
claimKey,
|
|
153
|
+
secret,
|
|
150
154
|
});
|
|
151
155
|
```
|
|
152
156
|
|
|
153
157
|
`batchClaim` does the whole sequence in the order the contract enforces:
|
|
154
|
-
read `validatorNonceById(keccak256(utf8(id)))` →
|
|
155
|
-
|
|
156
|
-
recovers to `validator()` before spending gas** → call `withdrawByValidator` repeatedly until
|
|
157
|
-
`liveDropCountById(id)` is 0, reusing the one signature.
|
|
158
|
+
resolve the anchor and pending drop keys → read `validatorNonceById(keccak256(utf8(id)))` →
|
|
159
|
+
request and verify the backend signature → call `withdrawUnmappedBatchByKeys` once.
|
|
158
160
|
|
|
159
161
|
Rules specific to this path:
|
|
160
162
|
|
|
161
163
|
- **The recipient is the SIWE-authenticated address**, not a parameter you choose. Passing a
|
|
162
164
|
different `recipient` fails with `SIGN_FAILED`. To claim to another wallet, do SIWE with it.
|
|
163
|
-
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
-
|
|
167
|
-
- `maxCount` is a page size and is **not** covered by the signature — that is why one signature
|
|
168
|
-
survives every page. Do not re-request a signature per page.
|
|
169
|
-
- `hasClaimedBefore === false` (from `listDrops`) means the first claim must be an individual
|
|
170
|
-
claim; do not offer batch claim yet. The SDK also enforces this on-chain: batch claim aborts
|
|
171
|
-
with `DROP_NOT_FOUND` when `claimedRecipientOf(id)` is zero.
|
|
172
|
-
- The SDK cross-checks the signed recipient against `claimedRecipientOf(id)` and refuses to send
|
|
173
|
-
when they differ (`SIGN_FAILED`). This is a client-side guard — whether the contract enforces
|
|
174
|
-
it is **not verified** (the source is unverified and the signature check runs first), so do not
|
|
175
|
-
weaken or bypass it.
|
|
165
|
+
- The anchor temporary wallet submits the transaction with the configured sponsored fee path.
|
|
166
|
+
- The anchor drop's `secret` and `claimKey` are required in addition to the validator signature.
|
|
167
|
+
- A successful batch consumes the nonce and signature. Request a fresh signature for another call.
|
|
168
|
+
- Each page gets a fresh nonce and validator signature; `maxCount` defaults to 30.
|
|
176
169
|
- On `SIGN_FAILED` the backend signed something the contract will reject — a different EIP-712
|
|
177
170
|
domain/type, a stale nonce, or a `personal_sign` prefix. Do not retry; report it.
|
|
178
171
|
|
|
@@ -182,8 +175,8 @@ The on-chain contract (verified against deployed bytecode, CROSS testnet 612044)
|
|
|
182
175
|
domain { name: 'ONEpop', version: '1', chainId, verifyingContract: <SafeDrop> }
|
|
183
176
|
type ValidatorClaim(address recipient,string id,uint256 nonce,uint256 deadline)
|
|
184
177
|
nonce validatorNonceById(bytes32 keccak256(utf8(id))) // hashed key, NOT the raw string
|
|
185
|
-
submit
|
|
186
|
-
|
|
178
|
+
submit withdrawUnmappedBatchByKeys(recipient, id, anchorDropKey, claimSignature,
|
|
179
|
+
secret, nonce, deadline, dropKeys, validatorSignature)
|
|
187
180
|
errors ValidatorSigExpired() → deadline passed | InvalidValidatorNonce() → stale nonce
|
|
188
181
|
ECDSAInvalidSignature() → signer is not validator()
|
|
189
182
|
```
|
|
@@ -207,33 +200,31 @@ The SDK does not wrap it — use `EventSource` directly. Delivery is best-effort
|
|
|
207
200
|
logs, no analytics, no backend call. Sending it collapses the trust split.
|
|
208
201
|
3. **`getJwt` is mandatory** for `deposit`, `listDrops`, `listHistories`, and all `x-connections`
|
|
209
202
|
calls. Without it those reject with `UNAUTHORIZED` before any request is sent.
|
|
210
|
-
4.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
203
|
+
4. **The current deployment requires EIP-2612 permit.** The token must support EIP-2612.
|
|
204
|
+
5. **Keep `transactionFees` as EIP-1559 with tip ≥ 1 gwei.** Every write supplies an explicit
|
|
205
|
+
gas limit. Batch gas is `batchBaseGas + batchGasPerDrop * count` and may not exceed
|
|
206
|
+
`maxBatchGas`; calibrate these values below the chain block gas limit.
|
|
214
207
|
6. **`message` ≤ 140 runes** (code points) and `envelopeId` must come from `listEnvelopes()`;
|
|
215
208
|
otherwise the backend rejects with `INVALID_PARAM` / `ENVELOPE_NOT_FOUND`.
|
|
216
209
|
7. **Handles are normalized** by the SDK (`normalizeHandle`); pass the same handle to deposit and
|
|
217
210
|
to the API — do not hand-roll a second normalization.
|
|
218
|
-
8.
|
|
219
|
-
returns (the permit deployment returns five fields, no expiry). `expiry === undefined` means
|
|
220
|
-
*unknown*, not *not expired* — never drive refund/expiry UI off a missing value.
|
|
211
|
+
8. **Drops are keyed by bytes32.** Resolve `activeDropOf(claimAddress)` before `drops(dropKey)`.
|
|
221
212
|
|
|
222
213
|
## 7. API surface
|
|
223
214
|
|
|
224
215
|
| `safeDrop` method | Endpoint / chain call | Auth |
|
|
225
216
|
|---|---|---|
|
|
226
|
-
| `deposit` | `POST /wallets` + `
|
|
217
|
+
| `deposit` | `POST /wallets` + permit `deposit` | JWT |
|
|
227
218
|
| `retrieveClaimKey` | `POST /wallets/private-key` | X OAuth token (body) |
|
|
228
219
|
| `withdraw` | `withdraw(...)` from the claim wallet | claim key |
|
|
229
|
-
| `refund` | `
|
|
230
|
-
| `getDrop` | `
|
|
220
|
+
| `refund` | `activeDropOf(claimAddr)` + `refund(dropKey)` | sponsor wallet |
|
|
221
|
+
| `getDrop` | `activeDropOf(claimAddr)` + `drops(dropKey)` | — |
|
|
231
222
|
| `listDrops` | `GET /drops` | JWT + active X connection |
|
|
232
223
|
| `listEnvelopes` | `GET /envelopes` | public |
|
|
233
224
|
| `getLeaderboard` | `GET /leaderboards?token=` | public |
|
|
234
225
|
| `listHistories` | `GET /histories` | JWT |
|
|
235
226
|
| `getXConnection` / `connectX` / `disconnectX` | `GET`/`POST`/`DELETE /x-connections` | JWT |
|
|
236
|
-
| `batchClaim` | `POST /batch-claim-signature` + `
|
|
227
|
+
| `batchClaim` | `POST /batch-claim-signature` + `withdrawUnmappedBatchByKeys` | JWT + X OAuth token + anchor claim key |
|
|
237
228
|
| `requestBatchClaimSignature` | `POST /batch-claim-signature` (low-level; prefer `batchClaim`) | JWT + X OAuth token |
|
|
238
229
|
|
|
239
230
|
## 8. Errors
|
|
@@ -265,4 +256,4 @@ Everything rejects with `SafeDropError` — branch on `err.code`, never on the m
|
|
|
265
256
|
`buildWithdrawLink` (the `#fragment` placement is load-bearing).
|
|
266
257
|
- Injecting a partial `SafeDropApiPort` mock and calling the query methods → `API_ERROR`
|
|
267
258
|
("not implemented"); implement the methods your test path touches.
|
|
268
|
-
-
|
|
259
|
+
- Using a token without EIP-2612 permit support.
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ npm i @nexus-cross/pop viem # react는 선택
|
|
|
24
24
|
|
|
25
25
|
## 개념
|
|
26
26
|
|
|
27
|
-
- **claimAddress** — 드롭당 임시 지갑 주소.
|
|
27
|
+
- **claimAddress** — 드롭당 임시 지갑 주소. `activeDropOf`로 현재 drop key를 찾는 데 사용.
|
|
28
28
|
- **secret** — 평문 비밀 문구. **어떤 서버에도 전송되지 않고** 수령 링크의 `#fragment`에만 담긴다.
|
|
29
29
|
- **withdrawLink** — `…/claim?sender=<addr>#<secret>`. 이 링크를 X DM으로 수령인에게 보낸다.
|
|
30
30
|
- **claimKey** — 임시 claim 지갑의 개인키. 수령인이 X OAuth 검증 후 백엔드에서 받아 가스 대납 withdraw에 쓴다.
|
|
@@ -41,12 +41,11 @@ const publicClient = createPublicClient({ chain, transport: http() });
|
|
|
41
41
|
const walletClient = createWalletClient({ chain, transport: custom(window.ethereum) }); // 연결된 지갑
|
|
42
42
|
|
|
43
43
|
const safeDrop = createSafeDropClient({
|
|
44
|
-
contractAddress: '0x…SafeDrop',
|
|
45
44
|
publicClient,
|
|
46
45
|
walletClient,
|
|
47
46
|
chain,
|
|
48
47
|
claimBaseUrl: 'https://one-pop.example/claim',
|
|
49
|
-
//
|
|
48
|
+
// API와 ONEpop 주소는 VITE_/NEXT_PUBLIC_ONE_POP_ENVIRONMENT로 함께 결정
|
|
50
49
|
api: { getJwt: async () => siweJwt }, // deposit(createClaimWallet)에 SIWE JWT 필요
|
|
51
50
|
});
|
|
52
51
|
```
|
|
@@ -65,7 +64,9 @@ const { token } = await auth.siweToken(address, signature); // token = getJwt
|
|
|
65
64
|
## 1) Deposit — 토큰 보내기
|
|
66
65
|
|
|
67
66
|
```ts
|
|
68
|
-
|
|
67
|
+
import { buildComposeUrl, buildDmText } from '@nexus-cross/pop';
|
|
68
|
+
|
|
69
|
+
const deposit = await safeDrop.deposit({
|
|
69
70
|
sender: address, // 연결된 지갑 = 스폰서
|
|
70
71
|
recipient: { provider: 'x', handle: 'theo_13303' },
|
|
71
72
|
token: '0x…ERC20',
|
|
@@ -74,18 +75,16 @@ const { withdrawLink, claimAddress } = await safeDrop.deposit({
|
|
|
74
75
|
envelopeId: 'classic-coral', // 선택 — listEnvelopes()의 id
|
|
75
76
|
// secret 미지정 시 자동 생성
|
|
76
77
|
});
|
|
78
|
+
if (!deposit.isMapped) {
|
|
79
|
+
location.assign(buildComposeUrl(buildDmText(deposit.withdrawLink), recipientId));
|
|
80
|
+
}
|
|
77
81
|
```
|
|
78
82
|
|
|
79
83
|
`message`·`envelopeId`는 **백엔드에만 저장**되는 메타데이터다(컨트랙트에 필드 없음).
|
|
80
84
|
|
|
81
|
-
내부 흐름: 임시 claim 지갑 생성(백엔드) →
|
|
82
|
-
|
|
83
|
-
수령인에게 링크 전달 (X DM 작성창):
|
|
85
|
+
내부 흐름: 임시 claim 지갑 생성(백엔드) → EIP-2612 permit 서명 → `deposit` → 링크 생성. `amount`는 **BigInt로 끝까지** 유지한다 (Number 변환 금지).
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
import { buildDmText, buildComposeUrl } from '@nexus-cross/pop';
|
|
87
|
-
location.assign(buildComposeUrl(buildDmText(withdrawLink), recipientId));
|
|
88
|
-
```
|
|
87
|
+
`isMapped: true`면 이미 매핑된 수령인으로 즉시 예치되어 claim link가 없다.
|
|
89
88
|
|
|
90
89
|
## 2) Withdraw — 수령인이 받기
|
|
91
90
|
|
|
@@ -95,7 +94,7 @@ location.assign(buildComposeUrl(buildDmText(withdrawLink), recipientId));
|
|
|
95
94
|
import { parseWithdrawLink } from '@nexus-cross/pop';
|
|
96
95
|
import { XAuthClient, HttpXAuthAdapter } from '@nexus-cross/pop/adapters';
|
|
97
96
|
|
|
98
|
-
const { sender, secret } = parseWithdrawLink(location.href);
|
|
97
|
+
const { sender, claimAddress, secret } = parseWithdrawLink(location.href);
|
|
99
98
|
|
|
100
99
|
// (a) X OAuth2 PKCE — 리다이렉트는 앱이 담당
|
|
101
100
|
const xauth = new XAuthClient({
|
|
@@ -108,37 +107,38 @@ const xauth = new XAuthClient({
|
|
|
108
107
|
const { oauth } = await xauth.complete(location.href);
|
|
109
108
|
|
|
110
109
|
// (b) OAuth 검증 → 임시 claim 키/주소 수령 (point of return)
|
|
111
|
-
const
|
|
110
|
+
const claim = await safeDrop.retrieveClaimKey({
|
|
111
|
+
claimAddress: claimAddress!,
|
|
112
112
|
senderAddress: sender!,
|
|
113
113
|
oauth,
|
|
114
114
|
});
|
|
115
|
+
if (claim.isMapped) throw new Error('Use the mapped withdrawal flow');
|
|
115
116
|
|
|
116
117
|
// (c) 가스 대납 withdraw — 임시 지갑이 직접 tx 전송
|
|
117
118
|
const { txHash } = await safeDrop.withdraw({
|
|
118
119
|
recipient: myReceivingAddress, // 수령인이 지정, 서명에 바인딩
|
|
120
|
+
sponsor: sender!,
|
|
119
121
|
claimAddress,
|
|
120
122
|
secret: secret!,
|
|
121
|
-
claimKey,
|
|
123
|
+
claimKey: claim.claimKey,
|
|
122
124
|
});
|
|
123
125
|
```
|
|
124
126
|
|
|
125
|
-
## 3) Refund —
|
|
127
|
+
## 3) Refund — sponsor 환불
|
|
126
128
|
|
|
127
129
|
```ts
|
|
128
|
-
await safeDrop.refund({ claimAddress });
|
|
130
|
+
await safeDrop.refund({ claimAddress, sponsor: address });
|
|
129
131
|
```
|
|
130
132
|
|
|
131
133
|
## 조회 — 드롭 상태 (온체인)
|
|
132
134
|
|
|
133
135
|
```ts
|
|
134
|
-
const drop = await safeDrop.getDrop(claimAddress); // 없으면 null
|
|
135
|
-
// drop: DropState { claimAddress, sponsor, amount, secretHash,
|
|
136
|
+
const drop = await safeDrop.getDrop(claimAddress, sponsor); // 없으면 null
|
|
137
|
+
// drop: DropState { dropKey, claimAddress, sponsor, recipient, amount, secretHash, id, token }
|
|
136
138
|
```
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
`token`은 에스크로 `token()`으로 채우지만, **`expiry`는 5필드 배포본에서 `undefined`** 다.
|
|
141
|
-
만료 판정 전에 존재 여부를 확인하라(`undefined` = 만료 아님이 아니라 *알 수 없음*).
|
|
140
|
+
현재 컨트랙트는 `activeDropOf(claimAddress)`로 key를 찾고 `drops(dropKey)`를 조회한다.
|
|
141
|
+
토큰은 드롭별 필드가 아니라 에스크로의 고정 `token()` 값이다.
|
|
142
142
|
|
|
143
143
|
## 4) 백엔드 조회 — 인박스 · X 연결 · 리더보드 · 히스토리
|
|
144
144
|
|
|
@@ -152,15 +152,15 @@ await safeDrop.disconnectX(); // idempotent
|
|
|
152
152
|
|
|
153
153
|
// 내게 온 수령 대기 목록 (Bearer JWT + 활성 X 연결)
|
|
154
154
|
const inbox = await safeDrop.listDrops({ token: '0x…ERC20' });
|
|
155
|
-
// { items
|
|
156
|
-
// PendingDrop { claimAddress, token, amount
|
|
155
|
+
// { items, count, totalAmount, hasClaimedBefore, mappedRecipient, pendingChangeTo }
|
|
156
|
+
// PendingDrop { id, dropKey, claimAddress, token, amount, message, envelopeId, depositedAt, sender }
|
|
157
157
|
|
|
158
158
|
// 카드 디자인 · 리더보드 (공개 — JWT 불필요)
|
|
159
159
|
const envelopes = await safeDrop.listEnvelopes({ locale: 'ko' });
|
|
160
|
-
const top = await safeDrop.getLeaderboard({ token: '0x…ERC20' });
|
|
160
|
+
const top = await safeDrop.getLeaderboard({ token: '0x…ERC20', page: 1, pageSize: 10 });
|
|
161
161
|
|
|
162
|
-
// 내
|
|
163
|
-
const page = await safeDrop.listHistories({
|
|
162
|
+
// 내 drop 히스토리
|
|
163
|
+
const page = await safeDrop.listHistories({ type: 'received', page: 1, pageSize: 20 });
|
|
164
164
|
```
|
|
165
165
|
|
|
166
166
|
`inbox.hasClaimedBefore === false`면 **첫 수령은 개별 claim만** 가능하다 — 일괄 수령 CTA를 이 값으로 게이트한다.
|
|
@@ -169,26 +169,25 @@ const page = await safeDrop.listHistories({ event: 'Withdrawn', page: 1, pageSiz
|
|
|
169
169
|
|
|
170
170
|
```ts
|
|
171
171
|
const { txHashes, recipient } = await safeDrop.batchClaim({
|
|
172
|
-
id: myHandle,
|
|
173
|
-
oauth,
|
|
174
|
-
|
|
172
|
+
id: myHandle,
|
|
173
|
+
oauth,
|
|
174
|
+
sponsor: sender,
|
|
175
|
+
claimAddress,
|
|
176
|
+
claimKey,
|
|
177
|
+
secret,
|
|
175
178
|
});
|
|
176
179
|
```
|
|
177
180
|
|
|
178
|
-
컨트랙트가 강제하는 순서대로 진행한다:
|
|
179
|
-
`validatorNonceById
|
|
180
|
-
`
|
|
181
|
-
복원되는지 가스 쓰기 전에 검증** → `liveDropCountById(id)`가 0이 될 때까지
|
|
182
|
-
`withdrawByValidator` 반복(서명 1건 재사용).
|
|
181
|
+
컨트랙트가 강제하는 순서대로 진행한다: anchor drop key와 pending keys 조회 →
|
|
182
|
+
`validatorNonceById` → 백엔드 서명 → 서명자 검증 →
|
|
183
|
+
`withdrawUnmappedBatchByKeys` 1회 호출. validator 서명은 성공한 호출마다 소비된다.
|
|
183
184
|
|
|
184
185
|
| 개별 수령과 다른 점 | 내용 |
|
|
185
186
|
|---|---|
|
|
186
|
-
| secret |
|
|
187
|
+
| secret | anchor drop의 secret/claim key가 필요하다 |
|
|
187
188
|
| 수령 주소 | **SIWE 인증 주소로 고정**. 다른 주소를 넘기면 `SIGN_FAILED`(그 주소로 SIWE 재로그인해야 함) |
|
|
188
|
-
| tx 전송자 |
|
|
189
|
-
| 서명 재사용 |
|
|
190
|
-
| 첫 수령 | 개별 claim이 선행돼야 한다 — 온체인 `claimedRecipientOf(id)`가 zero면 `DROP_NOT_FOUND`로 차단 |
|
|
191
|
-
| 목적지 교차검증 | 서명된 recipient ≠ `claimedRecipientOf(id)`면 서명이 유효해도 전송하지 않음(`SIGN_FAILED`) |
|
|
189
|
+
| tx 전송자 | anchor 임시 claim 지갑 |
|
|
190
|
+
| 서명 재사용 | 불가 — nonce는 성공한 batch 호출마다 소비됨 |
|
|
192
191
|
|
|
193
192
|
`SIGN_FAILED`가 나면 백엔드 서명이 컨트랙트가 기대하는 값과 다르다(EIP-712 domain/type 불일치,
|
|
194
193
|
낡은 nonce, `personal_sign` 프리픽스). 재시도해도 동일하게 실패하니 그대로 노출한다.
|
|
@@ -200,9 +199,8 @@ const { txHashes, recipient } = await safeDrop.batchClaim({
|
|
|
200
199
|
domain { name: 'ONEpop', version: '1', chainId, verifyingContract: <SafeDrop> }
|
|
201
200
|
type ValidatorClaim(address recipient,string id,uint256 nonce,uint256 deadline)
|
|
202
201
|
nonce validatorNonceById(bytes32) ← keccak256(utf8(id)). **원문 string 아님**
|
|
203
|
-
submit
|
|
204
|
-
|
|
205
|
-
고정 claimedRecipientOf(string id) → 개별 수령으로 확정된 주소(미수령이면 zero)
|
|
202
|
+
submit withdrawUnmappedBatchByKeys(recipient, id, anchorDropKey, claimSignature,
|
|
203
|
+
secret, nonce, deadline, dropKeys, validatorSignature)
|
|
206
204
|
검증 순서 deadline(ValidatorSigExpired) → nonce(InvalidValidatorNonce) → 서명(ECDSAInvalidSignature)
|
|
207
205
|
```
|
|
208
206
|
|
|
@@ -211,8 +209,7 @@ submit withdrawByValidator(address recipient, string id, uint256 nonce,
|
|
|
211
209
|
두 조건을 클라이언트에서 선제 차단하지만 온체인 강제의 대체물은 아니다 —
|
|
212
210
|
`docs/pop/02-frontend-dependencies.md#검증-한계-중요` 참조.
|
|
213
211
|
|
|
214
|
-
|
|
215
|
-
`adapters/validatorAbi.ts`에 바이트코드로 검증한 ABI를 따로 들고 있다.
|
|
212
|
+
전체 현재 ABI는 `safedrop_abi.json`에서 직접 export된다.
|
|
216
213
|
서명만 직접 받아 쓰려면 저수준 `safeDrop.requestBatchClaimSignature({ id, oauth, nonce, deadline })`.
|
|
217
214
|
</details>
|
|
218
215
|
|
|
@@ -245,7 +242,11 @@ Provider는 client를 context로 넣기만 한다 — 연결 로직은 없다.
|
|
|
245
242
|
| `ONE_POP_ENVIRONMENT` | 위 environment의 서버 전용(SSR, 접두사 없음) 변형 |
|
|
246
243
|
| `VITE_CROSS_AUTH_URL` / `NEXT_PUBLIC_CROSS_AUTH_URL` | cross-auth base URL override |
|
|
247
244
|
|
|
248
|
-
|
|
245
|
+
environment 하나로 one-pop-api와 ONEpop/Permit ERC20/NFT/Validator/FeeRecipient 주소를 함께 고른다.
|
|
246
|
+
dev/stage 주소는 패키지에 내장되어 있으며 production contract는 아직 미설정이므로 production에서는
|
|
247
|
+
`contractAddress`를 직접 넘겨야 한다. API override 미지정 시 environment의 기본 URL을 사용한다.
|
|
248
|
+
base URL은 https(또는 `http://localhost`)만 허용한다. 프로토콜 수수료는 5% 고정이며
|
|
249
|
+
`ONE_POP_FEE_BPS`(`500n`)와 `ONE_POP_BPS_DENOMINATOR`(`10_000n`)를 제공한다.
|
|
249
250
|
|
|
250
251
|
## 에러
|
|
251
252
|
|
|
@@ -279,13 +280,14 @@ override 미지정 시 environment로 기본 URL을 고른다. base URL은 https
|
|
|
279
280
|
|
|
280
281
|
| 옵션 | 타입 | 기본 | 설명 |
|
|
281
282
|
|---|---|---|---|
|
|
282
|
-
| `contractAddress` | `Address` |
|
|
283
|
+
| `contractAddress` | `Address` | environment의 ONEpop 주소 | 테스트/별도 배포 시 override. production 주소 미설정 상태에서는 필수 |
|
|
283
284
|
| `publicClient` | viem `PublicClient` | — (필수) | 읽기·getDrop·digest |
|
|
284
|
-
| `walletClient` | viem `WalletClient` | — (필수) |
|
|
285
|
+
| `walletClient` | viem `WalletClient` | — (필수) | deposit/refund 서명(연결 지갑) |
|
|
285
286
|
| `chain` | viem `Chain` | — (필수) | 대상 체인 |
|
|
286
287
|
| `claimBaseUrl` | `string` | — (필수) | 수령 링크 base URL |
|
|
287
288
|
| `transport` | viem `Transport` | `http()`(chain.rpcUrls) | withdraw 임시 지갑 client RPC |
|
|
288
|
-
| `
|
|
289
|
+
| `transactionFees` | `SafeDropTransactionFees` | 아래 gas-abstraction 기본 | 모든 write의 EIP-1559/gas override |
|
|
290
|
+
| `withdrawFees` | `SafeDropTransactionFees` | — | deprecated alias |
|
|
289
291
|
| `api` | `HttpSafeDropApiAdapterOptions` | — | one-pop-api 어댑터 옵션(아래) |
|
|
290
292
|
| `apiPort` | `SafeDropApiPort` | — | 직접 구현/mock 주입(지정 시 `api` 무시) |
|
|
291
293
|
|
|
@@ -298,17 +300,23 @@ override 미지정 시 environment로 기본 URL을 고른다. base URL은 https
|
|
|
298
300
|
| `fetchImpl` | `typeof fetch` | `globalThis.fetch` | 테스트/SSR 주입 |
|
|
299
301
|
| `paths` | `Partial<PopApiPaths>` | `DEFAULT_POP_API_PATHS` (`/wallets`, `/wallets/private-key`, `/drops`, `/envelopes`, `/leaderboards`, `/histories`, `/x-connections`, `/batch-claim-signature`, `/events/subscribe`) | 엔드포인트 경로 override |
|
|
300
302
|
|
|
301
|
-
### `ViemSafeDropChainAdapter.
|
|
303
|
+
### `ViemSafeDropChainAdapter.transactionFees` 기본값
|
|
302
304
|
|
|
303
|
-
|
|
305
|
+
모든 write는 `type: 'eip1559'`와 gas limit을 명시해 wallet/RPC gas estimation을 피한다.
|
|
306
|
+
batch gas는 `batchBaseGas + batchGasPerDrop × count`이며 `maxBatchGas` 초과 시 전송 전에 실패한다.
|
|
304
307
|
|
|
305
308
|
```
|
|
306
|
-
gas:
|
|
309
|
+
gas: 1_000_000n
|
|
310
|
+
batchBaseGas: 1_000_000n
|
|
311
|
+
batchGasPerDrop: 750_000n
|
|
312
|
+
maxBatchGas: 30_000_000n
|
|
307
313
|
maxFeePerGas: 4_000_000_000n // 4 gwei
|
|
308
314
|
maxPriorityFeePerGas: 1_000_000_000n // 1 gwei (노드 최소 tip)
|
|
309
315
|
```
|
|
310
316
|
|
|
311
|
-
|
|
317
|
+
기본 batch 30건은 `23_500_000` gas다(CROSS testnet block gas limit `105_000_000` 아래).
|
|
318
|
+
실제 배포 환경의 block gas limit과 실행량에 맞춰 네 값을 함께 조정해야 하며,
|
|
319
|
+
tip ≥ 1 gwei의 type-2를 유지해야 sponsor 과금이 동작한다.
|
|
312
320
|
|
|
313
321
|
### `CrossAuthClient(options)` — SIWE JWT 발급 (`getJwt`용)
|
|
314
322
|
|