diff --git a/.changeset/fix-dev-suri-cosmetic-phone-msg.md b/.changeset/fix-dev-suri-cosmetic-phone-msg.md new file mode 100644 index 00000000..9cb264e7 --- /dev/null +++ b/.changeset/fix-dev-suri-cosmetic-phone-msg.md @@ -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. diff --git a/src/utils/deploy/run.test.ts b/src/utils/deploy/run.test.ts index b4ebce1f..fdd9dc9c 100644 --- a/src/utils/deploy/run.test.ts +++ b/src/utils/deploy/run.test.ts @@ -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) }; @@ -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(); diff --git a/src/utils/deploy/run.ts b/src/utils/deploy/run.ts index 64915f5a..53a4af0d 100644 --- a/src/utils/deploy/run.ts +++ b/src/utils/deploy/run.ts @@ -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) {