Remote Proxies For... Everyone!

A few weeks ago, @bkchr published a post titled: Remote Proxies For The Braves. In it, he shared a PJS snippet to demonstrate how to send a remote proxy transaction.

However -as he already warned- the example is meant for the brave. While it technically works, it has a number of caveats and pitfalls that can cause transactions to fail unpredictably. I’ll explain those pitfalls in detail shortly.

But first, let’s look at a better alternative.


Remote Proxies Made Simple with PAPI

Using PAPI, you can create a Remote Proxy Transaction that’s significantly more robust and simpler to implement. Here’s how easy it is:

import { createClient, Enum } from "polkadot-api"
import { connectInjectedExtension } from "polkadot-api/pjs-signer"
import { getWsProvider } from "polkadot-api/ws-provider/web"
import { getRemoteProxySdk } from "@polkadot-api/sdk-remote-proxy"

const relayClient = createClient(getWsProvider("wss://rpc.ibp.network/kusama"))
const ahClient = createClient(getWsProvider("wss://sys.ibp.network/statemine"))
const sdk = getRemoteProxySdk(relayClient, ahClient)

// In this example we are using a browser-extension account, but this would
// also work with any other PolkadotSigner (ledger-signer, a raw-signer, etc)
const account = (await connectInjectedExtension("talisman"))
  .getAccounts()
  .find((x) => x.name === "YourAccountsName")!

// The relay-chain proxy account which is controled by the `account`
const PROXIED_ACCOUNT = "F5TNY7m4sntpbzEKvid6HZhktUXLWEEmqwVZwYXS9ZXKDf2"

// The signer that will take care of all the complexity
const signer = sdk.getProxiedSigner(PROXIED_ACCOUNT, account.polkadotSigner)

// The call that will be executed by the proxied account
const tx = ahClient.getUnsafeApi().tx.Balances.transfer_all({
  dest: Enum("Id", account.address),
  keep_alive: false,
})
// Let's sign and submit our tx... And we are done!
tx.signSubmitAndWatch(signer).subscribe(console.log, console.error)

:bullseye: Try it live on StackBlitz

That’s it! And yes-it really is that simple.

Behind the scenes, this snippet handles a variety of edge cases and failure modes that the PJS example does not. But more on that in the final section.


What About Multisigs?

As @bkchr notes in the last section of his post, this same approach wouldn’t work for remote proxies controlled by multisigs.

“The time between generating the proof and execution of the transaction on chain can only be 1 minute in maximum, this would not work for multisigs which need to coordinate via multiple people.”

No worries. Our PAPI SDK has you covered there too.

Here’s how you do it with PAPI:

import { AccountId, Enum, createClient } from "polkadot-api"
import { connectInjectedExtension } from "polkadot-api/pjs-signer"
import { getWsProvider } from "polkadot-api/ws-provider/web"
import { getRemoteProxySdk } from "@polkadot-api/sdk-remote-proxy"

const relayClient = createClient(getWsProvider("wss://rpc.ibp.network/kusama"))
const ahClient = createClient(getWsProvider("wss://sys.ibp.network/statemine"))
const remoteProxySdk = getRemoteProxySdk(relayClient, ahClient)

// In this example we are using a browser-extension account, but this would
// also work with any other PolkadotSigner like the ledger-signer, a raw-signer, etc
const account = (await connectInjectedExtension("talisman"))
  .getAccounts()
  .find((x) => x.name === "YourAccountsName")!

// The signer that will take care of all the complexity
const remoteMultisigProxySigner = remoteProxySdk.getMultisigProxiedSigner(
  // The relay-chain proxy account which is controled by the multisig
  "GeYsALZTCjRmG6bUCqtCK7t3mYk2B6JoaFGq2JiWxv5Yvzc",
  // The configuration of the multisig
  {
    signatories: [
      "DSDpapJC2viKE4nnAaDzEN1qP7NnNDVd3y4mVM2Ggu3nk4o",
      "DfQTJosoEyqD1uBqDo8TDGDdwmfgXXsQCLgLrofxyaV5zTm",
      "CzJobVBH3SLeNnmbNhVotUqBgsWvK5nAjfSQ9LmSxpT3583",
    ],
    threshold: 2,
  },
  // It has to be the signer of one of the `signatories`
  account.polkadotSigner,
)
// The SS58 address of the multisig
const multisigAddress = AccountId().dec(remoteMultisigProxySigner.accountId)

// The tx for sending the funds from the proxy account to the multisig
const tx = ahClient.getUnsafeApi().tx.Balances.transfer_all({
  dest: Enum("Id", multisigAddress),
  keep_alive: false,
})

// We can now sign and submit the transaction... and we are done!
// We just have to wait for another signatory to do the same
tx.signSubmitAndWatch(remoteMultisigProxySigner).subscribe(
  console.log,
  console.error,
)

:bullseye: Try this multisig demo on StackBlitz

What This Handles for You

The SDK’s getMultisigProxiedSigner() method wraps all of the following complexities:

  • Multisig Preparation

    • Creates Multisig.approve_as_multi calls.
    • Sorts signatories.
    • Calculates appropriate call weights.
  • Execution Wrapping

    • Final transaction is wrapped in Multisig.as_multi inside Utility.batch_all.
    • Includes the register_remote_proxy_proof call.
  • Mortality Management

    • Ensures the CheckMortality extension reflects proper block context.
    • Applies a short expiration window to prevent wasted fees on failure.

In short: you focus on logic, PAPI handles the tough parts.


Why This Is Even Possible

This works so smoothly because PAPI’s public interfaces are composable and decoupled from PAPI internals. They’re not only designed for PAPI itself-but also to interoperate with any compatible tool or library.

Here’s an example: PAPI’s public Signer interface. As you’ll see, it doesn’t expose any internal PAPI-specific details.

That means other libraries can:

  • Reuse our signers and signer-enhancers.
  • Build on top of them.
  • Compose and enhance them for cross-cutting concerns.

It’s unfortunate more libraries haven’t adopted the standardized interfaces we’ve promoted for over two years.


The Magic Behind the Scenes

Let’s circle back to what makes PAPI more robust than the raw PJS approach.

Handling Relay Chain Drift

What happens if, while fetching the proof from the relay chain, the latest block reported on the RemoteProxyRelayChain.BlockToRoot storage entry of the parachain moves forward by 4 blocks?

This isn’t theoretical, it actually happens fairly often, check out what happened between blocks 9,723,687 and 9,723,688. If you check the ParachainSystem.set_validation_data inherent, then you will see that the relay_parent_number field increased from 28,838,143 to 28,838,147.

That’s annoying. As that new block greatly decreased the chances for your transaction to enter inside the possible window. In a case like this, PAPI will cancel the request and re-fetch the proof for the most recent valid block (e.g. block 28838147), ensuring the transaction can be successfully included.

Protecting Against Reorgs

If there’s a re-org, your relay-chain proof might no longer be valid.

PAPI mitigates this by:

  • Aligning the CheckMortality block with the exact block for which the latest RemoteProxyRelayChain.BlockToRoot was queried.

  • Setting the mortality window to 8 blocks, ensuring the transaction is either:

    • Included successfully, or
    • Invalidated automatically (no wasted fees).

What About smoldot?

You might be wondering why the examples above use the WebSocket provider, when PAPI is generally a light-client-first library. Good catch!

The reason is that smoldot doesn’t yet support the specific RPC method required to query storage proofs.

That said, this is actively being addressed by the PAPI team. In fact, @voliva has already opened a PR to add support for this RPC to smoldot :flexed_biceps:.

But if you want to start using this feature right now with a light client, you don’t have to wait, we’ve published a “patched” version of smoldot that includes the necessary RPC support.


Final Thoughts

To sum up:

  • PAPI simplifies remote proxy and multisig proxy transactions.
  • It safely handles complex edge cases: relay block drift, reorgs, multisig intricacies, mortality improvements.
  • It uses fully composable and standardized public interfaces for greater interoperability.
  • And yes, light clients are still in the picture!
14 Likes

This is so good, thank you so very much!

Proxies are a key part of how the PDP (Polkadot Deployment Portal) handles rollups registrations. We’ll try it out and come back with feedback :))

4 Likes