Skip to content

Commit

Permalink
feat: Sign Messages without Exported Private Keys (leveraging WalletC…
Browse files Browse the repository at this point in the history
…onnet/Reown SDK)
  • Loading branch information
josiah-buxton committed Dec 6, 2024
1 parent 84dc54d commit 8fe9a06
Show file tree
Hide file tree
Showing 13 changed files with 2,125 additions and 109 deletions.
13 changes: 8 additions & 5 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,24 @@ BUNDLER_RPC_URL='https://polygon-amoy.g.alchemy.com/v2/<api-key>' ## Set with yo
SIGNER_ADDRESSES='
[
{
"address": "0x...",
"privateKey": "..."
"address": "0x..."
},
{
"address": "0x...",
"privateKey": "..."
"address": "0x..."
},
{
"address": "0x...",
"privateKey": "..."
"privateKey": "0x..."
}
]
'
LOG_LEVEL='info'

# WalletConnect
# You can find both of the following in your WallectConnect project settings
# We recommend create a new project
WC_PROJECT_ID='<project-id>' ## WalletConnect project ID

# Token transfer
RECIPIENT_ADDRESS='0x...'
TRANSFER_AMOUNT=0.001
Expand Down
2 changes: 2 additions & 0 deletions .licenseignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pkg:npm/[email protected]
pkg:npm/[email protected]
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

## 1.0.0 (2024-11-22)

### Features

* Add MSCA token transfer with exported private keys for signer wallets and signing util ([84dc54d](https://github.com/circlefin/msca-wallet-recovery/commit/84dc54dcefe212c57c25cf03779654b073183689))
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Demonstrates the following:
- USDC Token transfer
- Pay gas fee with wallet balance (native tokens)
- Note, gas fees are by default set to snapshot values from mempool when user op is built. This can lead to failures when broadcasting if gas fees rise before the user op is sent. You can modify `GAS_FEES_MULTIPLIER` env var to a value larger than 1 to pay more gas fees, which will make it more likely the token transfer will succeed and speed up the time it takes for the token transfer to be settled.
- Utility function for signing text with wallet private key
- Utility function for signing text with wallet

## Supported Blockchains

Expand Down Expand Up @@ -74,7 +74,7 @@ This section outlines the required environment variables that need to be set in
- Description: Set to true to enable broadcasting of operations, or false to disable it.
- `MSCA_WALLET_ADDRESS`
- Type: `string`
- Description: The address of the MSCA wallet address you are interfacing with.
- Description: The address of the MSCA wallet you are interfacing with.
- `BLOCKCHAIN`
- Type: `string`
- Description: The blockchain network to use. Options include:
Expand All @@ -89,13 +89,13 @@ This section outlines the required environment variables that need to be set in
- Description: The RPC URL for your blockchain provider (e.g. Infura, Alchemy). Replace <api-key> with your actual API key.
- `SIGNER_ADDRESSES`
- Type: `JSON string`
- Description: A JSON array containing objects with address and privateKey fields. Each object represents a signer address along with its associated private key. Example:
- Description: A JSON array containing objects with address and privateKey fields. Each object represents a signer address along with its associated private key. If private key is not provided, you will be prompted to connect your signer wallet by scanning a WalletConnect QR code when we need a signature from the signer. Example:

```json
```jsonc
[
{
"address": "0x...",
"privateKey": "..."
"privateKey": "..." // optional
}
]
```
Expand All @@ -108,6 +108,12 @@ This section outlines the required environment variables that need to be set in
- info (default)
- debug

#### WalletConnect

- `WC_PROJECT_ID`
- Type: `string`
- Description: The WalletConnect project ID. We use WalletConnect to connect your signer wallets and request for signatures. We recommend create a blank project for this tool.

#### Transfer

- `RECIPIENT_ADDRESS`
Expand All @@ -129,7 +135,7 @@ This section outlines the required environment variables that need to be set in

- `SIGNER_ADDRESS`
- Type: `string`
- Description: The address of the signer used to create the user operation. This address must exist in SIGNER_ADDRESSES to retrieve its private key.
- Description: The address of the signer used to create the user operation.
- `USER_OP_HASH`
- Type: `string`
- Description: The hash of the user operation (or any text) for signing.
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
"author": "Circle",
"license": "Apache-2.0",
"dependencies": {
"@walletconnect/ethereum-provider": "^2.17.2",
"command-line-args": "^6.0.0",
"dotenv": "^16.4.5",
"ethers": "^6.13.4",
"lodash": "^4.17.21",
"permissionless": "^0.1.31",
"qrcode-terminal": "^0.12.0",
"readline-sync": "^1.4.10",
"viem": "^2.13.10",
"winston": "^3.17.0"
Expand All @@ -28,6 +30,7 @@
"@types/command-line-args": "^5.2.3",
"@types/lodash": "^4.17.13",
"@types/readline-sync": "^1.4.8",
"@types/qrcode-terminal": "0.12.0",
"typescript": "^5.6.3"
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ const scenario: string = args[0];
default:
logAndExit(`Unrecognized scenario: ${scenario}`);
}
process.exit(0);
})();
29 changes: 15 additions & 14 deletions src/signHash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@

import commandLineArgs from "command-line-args";
import "dotenv/config";
import { ethers } from "ethers";
import { getAddress } from "viem";

import { getOptionValue } from "./utils/helpers.js";
import logger, { logAndExit, printSectionHeader } from "./utils/logger.js";
import { Signer } from "./utils/types.js";
import { signMessage } from "./utils/blockchain.js";
import { getEnvValue, getOptionValue } from "./utils/helpers.js";
import logger, { printSectionHeader } from "./utils/logger.js";
import { Address, NetworkKey, Signer } from "./utils/types.js";

const optionDefinitions = [
{ name: "blockchain", alias: "b", type: String },
{ name: "hash", alias: "h", type: String },
{ name: "address", alias: "a", type: String },
{ name: "signerAddresses", alias: "s", type: String },
Expand All @@ -37,28 +38,28 @@ export const signHash = async (argv: string[]): Promise<void> => {
printSectionHeader('Inputs');

// Get required input vars from CLI or environment
const blockchain = getOptionValue(options, "blockchain", "BLOCKCHAIN");
const userOpHash = getOptionValue(options, "hash", "USER_OP_HASH");
const signerAddress: string = getAddress(
getOptionValue(options, "address", "SIGNER_ADDRESS")
);
const signerAddresses: Signer[] = JSON.parse(
getOptionValue(options, "signerAddresses", "SIGNER_ADDRESSES")
);
const walletConnectProjectId = getEnvValue("WC_PROJECT_ID");

printSectionHeader('Signature');

// Check if the normalized address exists in the list of signer addresses
const foundSigner = signerAddresses.find(
const signer = signerAddresses.find(
(signer) => getAddress(signer.address) === signerAddress
);

if (!foundSigner || !foundSigner.privateKey) {
logAndExit(`Private key not found for signer address ${signerAddress} (not present in SIGNER_ADDRESSES env var or CLI arg or not structured like .env.sample file).`);
}

const signer = new ethers.Wallet(foundSigner!.privateKey, undefined);
) || {address: signerAddress as Address};

// Use bytes so that it will hash as eth signed message
const signature = await signer.signMessage(ethers.getBytes(userOpHash));
const signature = await signMessage({
chain: blockchain as NetworkKey,
signer: signer,
message: userOpHash,
walletConnectProjectId,
});
logger.info(`Signature: ${signature}`);
};
7 changes: 5 additions & 2 deletions src/tokenTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { createPublicClient, getAddress, http, parseUnits } from "viem";

import { buildAndSignMultisigUserOp, encodeCallData, getERC20TransferCallData } from "./utils/blockchain.js";
import { USDCTokenAddress, ViemChain } from "./utils/configs.js";
import { equalsIgnoreCase, getOptionValue } from "./utils/helpers.js";
import { equalsIgnoreCase, getEnvValue, getOptionValue } from "./utils/helpers.js";
import logger, { logAndExit, printSectionHeader } from "./utils/logger.js";
import { NetworkKey } from "./utils/types.js";
import { getAndCheckBalance } from "./utils/wallet.js";
Expand Down Expand Up @@ -80,6 +80,7 @@ export const tokenTransfer = async (argv: string[]): Promise<void> => {
"bundlerRPCUrl",
"BUNDLER_RPC_URL"
);
const walletConnectProjectId = getEnvValue("WC_PROJECT_ID");

if (equalsIgnoreCase(token, "native")) {
logAndExit("Native token transfer currently not supported.");
Expand Down Expand Up @@ -109,6 +110,7 @@ export const tokenTransfer = async (argv: string[]): Promise<void> => {
const userOperation = await buildAndSignMultisigUserOp({
chain,
bundlerRPCUrl,
walletConnectProjectId,
walletAddress,
signerAddresses,
gasFeesMultiplier,
Expand Down Expand Up @@ -144,7 +146,8 @@ export const tokenTransfer = async (argv: string[]): Promise<void> => {

// Token transfer
printSectionHeader('Token Transfer');

logger.info(`Sending the following userop`, userOperation);

// Handle broadcasting
if (broadcast) {
// Blocks on user input to confirm to send to blockchain
Expand Down
Loading

0 comments on commit 8fe9a06

Please sign in to comment.