Skip to content

Commit 39f6531

Browse files
tools: add drain-product-account (phone-signed PAS/PGAS mover for testing the funds drip) (#406)
1 parent 8b4226e commit 39f6531

1 file changed

Lines changed: 151 additions & 0 deletions

File tree

tools/drain-product-account.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (C) Parity Technologies (UK) Ltd.
2+
// SPDX-License-Identifier: Apache-2.0
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/**
16+
* drain-product-account.ts — move native PAS + PGAS out of one of YOUR
17+
* phone-controlled product accounts, signed on your phone via the live CLI
18+
* session. Built to put a product account below the playground-app funds
19+
* floors (native < 0.3 PAS AND PGAS < 5B) so the "Become a builder" resource
20+
* drip fires on a star — but it's a general "move funds between my own product
21+
* accounts" tool (set PRODUCT_ID + DEST to drain or refund).
22+
*
23+
* WHY THIS TOOL EXISTS / KEY INSIGHTS
24+
* - A product account (e.g. the playground-app's `5FNup4…`) is soft-derived
25+
* from the wallet root as `/product/{PRODUCT_ID}/{index}`. The SAME phone
26+
* signs for ANY product account of that root — you just point the signer at
27+
* the right PRODUCT_ID. Here, the playground.dot CLI session signs for a
28+
* DIFFERENT app's account (`pr-472-playgroundtest.dot/0`) by passing that
29+
* productId to createPlaygroundSessionSigner. Verified: same root
30+
* `5Ek9owHF…` → playground.dot/0 = 5CcaUS3…, pr-472-playgroundtest.dot/0 = 5FNup4….
31+
* - HARD REQUIREMENT: the CLI repo must be at a version whose host-papp can
32+
* DECODE your current session. The phone app writes SsoSessionsV3 with a
33+
* required `deviceEncPubKey` (host-papp 0.8.7 / product-sdk-terminal 0.5.0,
34+
* CLI ≥ v0.40.x). An older repo (e.g. v0.37 / host-papp 0.8.6) silently
35+
* fails to decode → getSessionSigner() returns null → "NO SESSION". If you
36+
* see that, `git checkout v0.40.2 && pnpm install` (or newer) first.
37+
* - Fees: paid in PGAS via ChargeAssetTxPayment (PGAS_FEE), matching how
38+
* product-account txs are charged. Pattern lifted from polkadot-app-deploy's
39+
* PGAS_FEE_OPTIONS (src/dotns.ts).
40+
*
41+
* RUN (from the playground-cli repo root, with the matching version installed):
42+
* bun tools/drain-product-account.ts # executes (2 phone taps)
43+
* DRY_RUN=1 bun tools/drain-product-account.ts # print plan only, no submit
44+
*
45+
* Each transfer is a phone approval — nothing moves without your tap.
46+
*/
47+
48+
import { getSessionSigner } from "../src/utils/auth.ts";
49+
import { createPlaygroundSessionSigner } from "../src/utils/sessionSigner.ts";
50+
import { createClient, Enum } from "polkadot-api";
51+
import { getWsProvider } from "polkadot-api/ws";
52+
import { paseo_asset_hub } from "@parity/product-sdk-descriptors/paseo-asset-hub";
53+
import { ss58Encode } from "@parity/product-sdk-address";
54+
55+
// ── config — edit these ────────────────────────────────────────────────────
56+
/** Which product account to move funds OUT of (its productId/index off your
57+
* root). "pr-472-playgroundtest.dot" → 5FNup4… ; "playground.dot" → 5CcaUS3… */
58+
const PRODUCT_ID = "pr-472-playgroundtest.dot";
59+
const DERIVATION_INDEX = 0;
60+
/** Destination — one of YOUR other product accounts (funds stay recoverable). */
61+
const DEST = "5CcaUS3AKQyJaVLeFGTwBMJPPpoEcgS55g64Fws4UdLqbcmC";
62+
/** What to LEAVE behind (planck / PGAS units). Defaults drop below the app's
63+
* funds floors (0.3 PAS = 3e9 planck, 5B PGAS). Set 0n to sweep everything. */
64+
const LEAVE_PAS_PLANCK = 2_000_000_000n; // 0.2 PAS
65+
const LEAVE_PGAS = 1_000_000_000n; // ~1B PGAS (covers the in-flight tx fees)
66+
const ASSET_HUB_RPC = "wss://paseo-asset-hub-next-rpc.polkadot.io";
67+
// ───────────────────────────────────────────────────────────────────────────
68+
69+
const PGAS_ID = 2_000_000_000;
70+
const DRY_RUN = process.env.DRY_RUN === "1";
71+
const fmt = (p: bigint) => (Number(p) / 1e10).toFixed(4) + " PAS";
72+
73+
// ChargeAssetTxPayment → PGAS (PalletInstance 50, GeneralIndex = asset id).
74+
const PGAS_FEE = {
75+
customSignedExtensions: {
76+
ChargeAssetTxPayment: {
77+
value: {
78+
tip: 0n,
79+
asset_id: {
80+
parents: 0,
81+
interior: {
82+
type: "X2",
83+
value: [
84+
{ type: "PalletInstance", value: 50 },
85+
{ type: "GeneralIndex", value: BigInt(PGAS_ID) },
86+
],
87+
},
88+
},
89+
},
90+
},
91+
},
92+
};
93+
94+
const handle = await getSessionSigner();
95+
if (!handle) {
96+
console.error(
97+
"NO SESSION. Either run `playground login`, or the repo is too old to decode\n" +
98+
"your session (need host-papp 0.8.7 / CLI >= v0.40.x: `git checkout v0.40.2 && pnpm install`).",
99+
);
100+
process.exit(1);
101+
}
102+
103+
const signer = createPlaygroundSessionSigner(handle.userSession, {
104+
productId: PRODUCT_ID,
105+
derivationIndex: DERIVATION_INDEX,
106+
});
107+
const src = ss58Encode(signer.publicKey);
108+
console.log(`source ${PRODUCT_ID}/${DERIVATION_INDEX} -> ${src}`);
109+
console.log(`dest -> ${DEST}`);
110+
111+
const client = createClient(getWsProvider([ASSET_HUB_RPC]));
112+
const api = client.getTypedApi(paseo_asset_hub);
113+
const free = (await api.query.System.Account.getValue(src))?.data?.free ?? 0n;
114+
const pgas = (await api.query.Assets.Account.getValue(PGAS_ID, src))?.balance ?? 0n;
115+
console.log(`BEFORE native ${fmt(free)} PGAS ${pgas}`);
116+
117+
const pgasSend = pgas - LEAVE_PGAS;
118+
const pasSend = free - LEAVE_PAS_PLANCK;
119+
console.log(`PLAN send PGAS ${pgasSend} + PAS ${pasSend} (${fmt(pasSend)}) -> ${DEST}`);
120+
121+
if (DRY_RUN) {
122+
console.log("DRY_RUN=1 — nothing submitted.");
123+
client.destroy();
124+
handle.destroy();
125+
process.exit(0);
126+
}
127+
128+
if (pgasSend > 0n) {
129+
console.log("→ TX1 Assets.transfer PGAS (approve on phone)…");
130+
const r = await api.tx.Assets
131+
.transfer({ id: PGAS_ID, target: Enum("Id", DEST), amount: pgasSend })
132+
.signAndSubmit(signer, PGAS_FEE);
133+
console.log(` TX1 ${r.ok ? "OK" : "FAILED"} ${r.txHash}`);
134+
}
135+
if (pasSend > 0n) {
136+
console.log("→ TX2 Balances.transfer_keep_alive PAS (approve on phone)…");
137+
const r = await api.tx.Balances
138+
.transfer_keep_alive({ dest: Enum("Id", DEST), value: pasSend })
139+
.signAndSubmit(signer, PGAS_FEE);
140+
console.log(` TX2 ${r.ok ? "OK" : "FAILED"} ${r.txHash}`);
141+
}
142+
143+
const free2 = (await api.query.System.Account.getValue(src))?.data?.free ?? 0n;
144+
const pgas2 = (await api.query.Assets.Account.getValue(PGAS_ID, src))?.balance ?? 0n;
145+
console.log(`AFTER native ${fmt(free2)} PGAS ${pgas2}`);
146+
console.log(
147+
`below app floors? native<0.3PAS:${free2 < 3_000_000_000n} pgas<5B:${pgas2 < 5_000_000_000n}`,
148+
);
149+
client.destroy();
150+
handle.destroy();
151+
process.exit(0);

0 commit comments

Comments
 (0)