Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-dev-suri-cosmetic-phone-msg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"playground-cli": patch
---

Fixed misleading "📱 Approve on your phone" log line during `dot deploy --signer dev --suri X --playground`. The session-key funding step now signs directly with the dev keypair instead of wrapping it in the phone-mode signing-event proxy. Phone-session deploys are unchanged.
47 changes: 47 additions & 0 deletions src/utils/deploy/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ const fakeUserSigner: ResolvedSigner = {
destroy: vi.fn(),
};

const fakeDevSigner: ResolvedSigner = {
signer: {
publicKey: new Uint8Array(32).fill(1),
signTx: vi.fn(),
signBytes: vi.fn(),
},
address: "5Dev",
source: "dev",
destroy: vi.fn(),
};

function collectEvents(): { events: DeployEvent[]; push: (e: DeployEvent) => void } {
const events: DeployEvent[] = [];
return { events, push: (e) => events.push(e) };
Expand Down Expand Up @@ -625,6 +636,42 @@ describe("runDeploy — contracts phase", () => {
expect(pickFunderMock).not.toHaveBeenCalled();
});

it("ensureSessionFunded: underfunded + dev signer (source='dev') → uses signer directly, no phone events", async () => {
detectContractsTypeMock.mockReturnValue("foundry");
const { client, transferFactory } = makeFakeClient();
getConnectionMock.mockResolvedValue(client);
checkBalanceMock.mockResolvedValue({ free: 0n, sufficient: false });

const { events, push } = collectEvents();
await runDeploy({
projectDir: "/tmp/proj",
buildDir: "/tmp/proj/dist",
skipBuild: true,
domain: "my-app",
mode: "dev",
publishToPlayground: false,
userSigner: fakeDevSigner,
deployContracts: true,
onEvent: push,
});

// Transfer was submitted — the dev signer funded the session key.
const transferArg = transferFactory.mock.calls[0][0] as { value: bigint };
expect(transferArg.value).toBe(50_000_000_000n);

// The signer passed to submitAndWatch must be the raw dev signer — NOT
// a wrapSignerWithEvents proxy. Assert exact object identity.
const [, usedFunder] = submitAndWatchMock.mock.calls[0];
expect(usedFunder).toBe(fakeDevSigner.signer);

// No phone-tap lifecycle events should have been emitted.
const signingEvents = events.filter((e) => e.kind === "signing");
expect(signingEvents).toHaveLength(0);

// Funder-chain lookup must NOT have been consulted.
expect(pickFunderMock).not.toHaveBeenCalled();
});

it("ensureSessionFunded: underfunded + pure dev mode → picks a funder from the chain", async () => {
detectContractsTypeMock.mockReturnValue("foundry");
const { client } = makeFakeClient();
Expand Down
27 changes: 21 additions & 6 deletions src/utils/deploy/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,18 +317,33 @@ async function ensureSessionFunded(opts: {

emitInfo(`funding session key ${opts.sessionAddress}…`);

// Phone signer: user pays, with a lifecycle event so the TUI numbers the
// tap. Pure dev mode: pick the first funder in the chain that has enough
// PAS to cover the top-up. If every dev funder is drained, tell the user
// to switch to a mobile signer rather than silently falling back to
// anything that might race the drainer.
// Three-way branch based on who's funding the session key top-up:
//
// source === "session" Phone signer: user pays on-device. Wrap with
// lifecycle events so the TUI can number the tap
// and show "📱 Approve on your phone".
//
// source === "dev" Dev-with-SURI: a local keypair (--suri //Alice
// or a BIP-39 mnemonic) pretending to be the user.
// Signs immediately in-process — no human in the
// loop — so wrapping with phone-tap events would
// be misleading. Sign directly.
//
// null Pure dev mode (no --suri, no session): pick the
// first funder in the chain that has enough PAS.
// If every dev funder is drained, tell the user to
// switch to a mobile signer rather than silently
// falling back to anything that might race the drainer.
let funder: PolkadotSigner;
if (opts.userSigner) {
if (opts.userSigner?.source === "session") {
funder = wrapSignerWithEvents(opts.userSigner.signer, {
label: "Fund contract deploy session key",
counter: opts.counter,
onEvent: (event) => opts.onEvent({ kind: "signing", event }),
});
} else if (opts.userSigner) {
// Dev-with-SURI: sign directly, no lifecycle events.
funder = opts.userSigner.signer;
} else {
const picked = await pickFunder(opts.client, SESSION_FUND_AMOUNT + FUNDER_FEE_BUFFER);
if (!picked) {
Expand Down
Loading