Skip to content

Commit

Permalink
Cleanup wallet delegation docs
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienGllmt committed Apr 25, 2024
1 parent 64ddefe commit cba860f
Show file tree
Hide file tree
Showing 11 changed files with 180 additions and 131 deletions.
2 changes: 1 addition & 1 deletion docs/home/200-read-write-L2-state/1-base-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ The `PaimaParser` grammar supports UTF8, but generally has the following reserve
- `*` See [parallelism](./200-parallelism.md)
- `@` Implicitly use the address that submitted the wallet for [parallelism](./200-parallelism.md). That is, for parallelism purposes, `@x|a` is equivalent to `x|*wallet|a`, but no actual modifications is done to the onchain format
- `?` Optional entry
- `&` At the start of a message for [wallet delegation](../700-multichain-support/3-delegate-wallet/1-introduction.mdx)
- `&` At the start of a message for [wallet delegation](../700-multichain-support/2-wallet-layer/100-delegate-wallet/1-introduction.mdx)
- `|` , `=` Used to define the grammar
- `-` Used for PaimaParser array notation
- ASCII 0x02 and 0x03. Use for [batched-mode](./400-batched-mode.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/home/200-read-write-L2-state/10-read-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function parse(s: string): ParsedSubmittedInput {
}
```

## Calling the parser from your STF
## Calling the parser from your STF {#stf-function}

Paima works by updating your state machine whenever happens onchain - the most common case being that somebody interacted with your Paima L2 contract. The set of actions your state machine can react to are called the [Paima Primitives](../300-react-to-events/2-primitive-catalogue/1-introduction.md).

Expand Down
11 changes: 3 additions & 8 deletions docs/home/200-read-write-L2-state/600-autosign.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Auto-signing for apps

**Note**: wallets that use this feature are still in development

Requiring users to manually sign every transaction for every game is a terrible user experience not just because it breaks immersion constantly, but because it's incompatible with games where users play with a controller (which is how many users play games)

However, auto-signing can be dangerous. A game could steal all funds in your wallet without you explicitly approving it (either because the game was malicious, or because the game was hacked)
Expand Down Expand Up @@ -56,13 +54,13 @@ namespace:
### Wallets that plan to support this format
- MetaMask through the [Paima Session Snap](https://github.com/PaimaStudios/paima-session-snap)
- Flint Wallet ([link](https://flint-wallet.com/)) (for Milkomeda C1 only)
- Any wallet for a cryptocurrency supported by Paima's [wallet layer](../700-multichain-support/2-wallet-layer/1-introduction.mdx) through the [delegate functionality](../700-multichain-support/2-wallet-layer/100-delegate-wallet/1-introduction.mdx)
## 2. Game-specific auto-signing transactions
For games that do not have a batcher, or in cases where users prefer submitting transactions themselves, we still want to enable safe auto-sign behavior.
However, in this case, adding the security namespace would add a lot of useless data on-chain, so instead wallets will auto-sign and transaction where the `to` field is the contract hash of the game's L2 contract with some measure to avoid these dApps slowly draining the user's L1 balance.
However, in this case, adding the security namespace would add a lot of useless data on-chain, so instead wallets will auto-sign any transaction where the `to` field is the contract hash of the game's L2 contract (with some measure to avoid these dApps slowly draining the user's L1 balance).

### Wallets that plan to support this format

Expand All @@ -72,7 +70,4 @@ However, in this case, adding the security namespace would add a lot of useless

For some apps, simply auto-signing data may not be enough. Instead, to safely sign transactions for the app, you want some kind of session key for the game.

To do this, we plan to generate a deterministic session key for each game.

Wallets that plan to support this format:
- MetaMask through the [Paima Session Snap](https://github.com/PaimaStudios/paima-session-snap)
This can be achieved through the [delegate functionality](../700-multichain-support/2-wallet-layer/100-delegate-wallet/1-introduction.mdx). Since Paima's [wallet layer](../700-multichain-support/2-wallet-layer/1-introduction.mdx) supports raw private key formats for some chains like EVM, you can use persistent session key login systems like the one provided by [thirdweb](https://thirdweb.com/) with Paima Engine and have the user delegate their primary wallet to the session key.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Primitive List

Meta primitives are primitives built into Paima Engine that are trigger by engine-related functionality.

They do not typically require explicitly being enabled your [configuration](../1-introduction.md#configuration).

List of meta primitives:
1. Wallet delegation ([docs](../../../700-multichain-support/2-wallet-layer/100-delegate-wallet/1-introduction.mdx))
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label": "Meta Primitives"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Introduction

Delegate Wallet allows for a wallet address to be delegated to another wallet address.

In practice, a user can use wallet address `A` to play, but their real wallet address is `B`

Some use cases:
* **Better UX**: By using a "Local Wallet" within the browser and sending transactions through the "Batcher", so each transaction gets signed automatically.
* **Security**: Allowing players to have a "Burner Wallet" with limited funds to play on.
* **Security**: Reduce interaction with real wallet.
* **Account Recovery**: account can be re-delegated to a new wallet address.

This feature is optional and you can implement at any time.
However, it's good practice use `user_id` in the STF to identify the user, and not the wallet address in case you end up implementing wallet delegation later.

Feature notes:
- Delegation can be done after-the-fact (ex: you can play on a burner wallet, and connect it to a main wallet later)
- Delegations can be cancelled
- [Cycles](https://en.wikipedia.org/wiki/Cycle_(graph_theory)) are not allowed
- Main wallet state can be delegated to multiple burner wallets (ex: playing on multiple devices at once)

Security risk:
- Somebody could trick you into delegating your game state to them (steal all your in-game progress). To avoid this, the delegation transaction uses human-readable strings so the user can easily see what they are signing.
Original file line number Diff line number Diff line change
@@ -1,40 +1,51 @@
# Getting Started
# Integrate into your game

We are going to implement a workflow to delegate a Local Wallet to an EVM Wallet.
Here we share all the steps to integrate the wallet delegation system into a game

For this example, we will be delegating a locally generated keypair (using the [thirdweb](https://www.npmjs.com/package/@thirdweb-dev/wallets) wrapper for [ethers](https://www.npmjs.com/package/ethers)) to a user's EVM wallet (ex: [Rabby](https://rabby.io/))


## In your middleware

1. First implement a [Local Wallet](../wallet-layer/introduction#thirdweb-support)
1. First implement a [Local Wallet](../../2-wallet-layer/1-introduction.mdx#thirdweb-support)

2. Create a function to sign messages with your local wallet. `WalletConnectHelper` and `getActiveAddress` is provided by @paima/sdk. `LocalWallet` is provided by `thirdweb`.
2. Create a function to sign messages with your local wallet.
```js
import { getActiveAddress, WalletConnectHelper } from '@paima/sdk/mw-core';

const signMessageWithLocalWallet = async (message: string) => {
// wraps the message with the correct Paima Concise notation
const toSign = new WalletConnectHelper().buildMessageToSign(message);

return {
message: toSign,
// assumes you've called `userWalletLogin` on the local Ethers wallet from step (1)
walletAddress: getActiveAddress(WalletMode.EvmEthers).toLocaleLowerCase(),
/* localWallet is an instance of LocalWallet from the previous step */
/* localWallet is an instance of LocalWallet from thirdweb from step (1) */
signedMessage: await localWallet.signMessage(message),
};
};
```

3. Now create a function to get User Wallets. `allInjectedWallets` is provided by `@paima/sdk`.
3. Now create a function to get User Wallets
```js
import { allInjectedWallets } from '@paima/sdk/providers';

const getWallets = async () => {
/* "wallets" is a map of connected wallets */
const wallets = await allInjectedWallets({
gameName: 'GameName',
gameName: 'GameName', // replace with your game's name
gameChainId: undefined,
});
return wallets;
}
```

4. Create a final function to sign messages with an external wallet. `WalletConnectHelper` is provided by `@paima/sdk`.
4. Create a final function to sign messages with an external wallet.
```js
import { AddressType } from '@paima/sdk/utils';
import { WalletConnectHelper } from '@paima/sdk/mw-core';

const connectAndSignExternalWallet = async (
walletType: AddressType,
walletName: string, /* Provided in wallets[WalletMode].metadata.name */
Expand All @@ -44,8 +55,11 @@ const connectAndSignExternalWallet = async (
};
```

5. Put it all together. `walletConnectPromise` is provided by `@paima/sdk`.
5. Put it all together.
```js
import { walletConnect } from '@paima/sdk/mw-core';
import { WalletMode } from '@paima/sdk/providers';

const localWalletMessage = await signMessageWithLocalWallet();
const wallets = getWallets();

Expand All @@ -54,11 +68,11 @@ const wallets = getWallets();
const injectedWalletMessage = await connectAndSignExternalWallet(
AddressType.EVM,
wallets[WalletMode.EvmInjected].metadata.name,
localWalletSignedMessage.walletAddress
localWalletMessage.walletAddress
);

// Send message to blockchain.
await walletConnectPromise(
await walletConnect(
injectedWalletMessage.walletAddress,
null, // localWalletMessage.walletAddress,
injectedWalletMessage.signedMessage,
Expand All @@ -78,38 +92,54 @@ clearDelegateWalletCacheOnChanges(requirePersistentConnection());
```
This allows to clear the API cache when the user changes the wallet connection.

2. STF Changes
2. [STF](../../../200-read-write-L2-state/10-read-data.md#stf-function) Changes

As user wallet might change over time, as they can delegate, migrate and cancel delegations, we need to use `userId` instead of `userAddress` or `realAddress` for user identification in our STF.
```js
// main entry point for your game's state machine
export default async function (
inputData: SubmittedChainData,
blockHeight: number,
randomnessGenerator: Prando,
dbConn: Pool
): Promise<SQLUpdate[]> {
// highlight-start
/* use this user to identify the player instead of userAddress or realAddress */
const user = String(inputData.userId);
/* use user to identify user instead of userAddress or realAddress */
// highlight-end
...
}
```
* userAddress: contains the main wallet address.
* realAddress: contains the real wallet address that sent the transaction.
* userId: contains the user id, which is the same for all wallets of the same user.
* `userAddress`: contains the main wallet address.
* `realAddress`: contains the real wallet address that sent the transaction.
* `userId`: contains the user id, which is the same for all wallets of the same user.


3. API Controllers
In your API, if receiving user wallet address, convert them into the USER ID.
```js
import { getMainAddress } from '@paima/node-sdk/db';

// example query for a game that gets items for a user
@Route('items')
export class ItemController extends Controller {
@Get('/user')
public async getItemsForUser(
@Query() wallet: string
): Promise<{ items: IGetAllItemsForUserResult[] }> {
const pool = requirePool();

// highlight-start
// this is the main line (everything else is just an example context)
const address = await getMainAddress(wallet, pool);
/* now use address, instead of wallet */
const items = await getAllItemsForUser.run({ wallet: address }, pool);
// highlight-end

/* now use address (instead of wallet) */
const items = await getAllItemsForUser.run(
// highlight-next-line
{ wallet: address },
pool
);
return { items };
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Effect on Interfaces

Enabling wallet delegation has implications on the interfaces of multiple different parts of your application.

## Meta Primitive

The user must (using a [concise](../../../200-read-write-L2-state/1-base-format.md) command) send a transaction to delegate their wallet address to another wallet address

Wallet delegation provides the following [meta primitives](../../../300-react-to-events/2-primitive-catalogue/5-meta/100-introduction.md):
```
delegate = &wd|from?|to?|from_signature|to_signature
migrate = &wm|from?|to?|from_signature|to_signature
cancelDelegations = &wc|to_signature
```

## STF

[STF](../../../200-read-write-L2-state/10-read-data.md#stf-function) data is slightly changed:
* `userAddress`: contains the main wallet address.
* `realAddress`: contains the real wallet address that sent the transaction.
* `userId`: contains the user id, which is the same for all wallets of the same user.

`userAddress` and `realAddress` will be the same for the main wallet or if there are no delegations.

## Indexer Database

There are 2 tables tracked in the indexer to manage delegations:
* `addresses`: *(always present)* maps a unique user id to each wallet address.
* `delegations`: *(new for delegations)* maps delegated wallet addresses using IDs from the *addresses* table.

## Middleware

There are multiple files part of `@paima/sdk/mw-core` to help integrate wallet delegation

### Connect two wallets

This function delegates `from` your main wallet `to` your burner wallet

When delegating, you must sign the following message template in 2 steps: once for each key (`from` key & `to` key):
```
DELEGATE-WALLET:<other-address-lowercase>:<env-contract-address>
```
* `other-address-lowercase`: the address not signing this step
* `env-contract-address`: is defined in [.env](../../../1-setup/4-environment-config-values.md) (`CONTRACT_ADDRESS`)
* *Note:* the address signing for a given step is not explicitly added since its address & signature are implicitly part of the transaction


```js
function walletConnect(
// If from/to are null
// they gets replaced by the wallet that sends (or signs if using the batcher) the tx
from: string | null,
to: string | null,
from_signature: string,
to_signature: string
): Promise<SuccessfulResult<PostDataResponse> | FailedResult>
```

### Cancel delegation

This function removes any current delegation to the sender address.
The message has the format:
```js
DELEGATE-WALLET::<env-contract-address>
```

*Note*: address is omitted. You can do this by using `buildMessageToSign` with an empty string (`buildMessageToSign('')`)

```js
function walletConnectCancelDelegations(
to_signature: string
): Promise<SuccessfulResult<PostDataResponse> | FailedResult> {
```
### Migrate wallet
This function migrates the address `from` to `to`.
```js
function walletConnectMigrate(
// null gets replaced by the wallet that signs the tx
from: string | null,
to: string | null,
from_signature: string,
to_signature: string
): Promise<SuccessfulResult<PostDataResponse> | FailedResult>
```

### Helper

The `WalletConnectHelper` class contains helper functions to sign with injected or generate the message for local wallets:
1. `buildMessageToSign` to generate the correct concise notation to sign for an action
2. `connectExternalWalletAndSign` to connect an external wallet and delegate from it at the same time.

This file was deleted.

Loading

0 comments on commit cba860f

Please sign in to comment.