diff --git a/examples/suave-web-demo/src/main.ts b/examples/suave-web-demo/src/main.ts index fb5a9fbc..2c613205 100644 --- a/examples/suave-web-demo/src/main.ts +++ b/examples/suave-web-demo/src/main.ts @@ -4,7 +4,7 @@ import typescriptLogo from './typescript.svg' import flashbotsLogo from './flashbots_icon.svg' import { setupConnectButton, setupDripFaucetButton, setupSendBidButton } from './suave' import { Logo } from './components' -import { custom, formatEther } from 'viem' +import { custom, formatEther, http } from 'viem' import { getSuaveWallet, getSuaveProvider } from 'viem/chains/utils' import { suaveRigil } from 'viem/chains' @@ -38,7 +38,7 @@ setupConnectButton(document.querySelector('#connect')!, } const suaveWallet = getSuaveWallet({jsonRpcAccount: account, transport: custom(ethereum)}) console.log(suaveWallet) - const suaveProvider = getSuaveProvider(custom(ethereum)) + const suaveProvider = getSuaveProvider(http("http://localhost:8545")) suaveProvider.getBalance({ address: account }).then((balance: any) => { suaveProvider.getChainId().then((chainId: any) => { if (chainId !== suaveRigil.id) { diff --git a/examples/suave-web-demo/src/suave.ts b/examples/suave-web-demo/src/suave.ts index 4b6fae73..c00db4b4 100644 --- a/examples/suave-web-demo/src/suave.ts +++ b/examples/suave-web-demo/src/suave.ts @@ -6,7 +6,7 @@ import { http, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { suaveRigil, goerli } from 'viem/chains' +import { suaveRigil, holesky } from 'viem/chains' import { OFAOrder } from '../../suave/bids' import { getSuaveWallet } from 'viem/chains/utils' import BidContractDeployment from '../../suave/deployedAddress.json' @@ -16,18 +16,18 @@ const KETTLE_ADDRESS: Address = '0xb5feafbdd752ad52afb7e1bd2e40432a485bbb7f' const ADMIN_KEY: Hex = '0x91ab9a7e53c220e6210460b65a7a3bb2ca181412a8a7b43ff336b3df1737ce12' // public goerli node, may need to change if it goes down: -const GOERLI_RPC_URL_HTTP: string = 'https://goerli.rigil.suave.flashbots.net' +const L1_RPC_URL_HTTP: string = 'https://holesky.rigil.suave.flashbots.net' const goerliWallet = createWalletClient({ account: privateKeyToAccount( '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', ), - chain: goerli, - transport: http(GOERLI_RPC_URL_HTTP), + chain: holesky, + transport: http(L1_RPC_URL_HTTP), }) const goerliProvider = createPublicClient({ - transport: http(GOERLI_RPC_URL_HTTP), - chain: goerli, + transport: http(L1_RPC_URL_HTTP), + chain: holesky, }) const suaveAdminWallet = getSuaveWallet({ privateKey: ADMIN_KEY, diff --git a/examples/suave/bids.ts b/examples/suave/bids.ts index c1969838..e35f2414 100644 --- a/examples/suave/bids.ts +++ b/examples/suave/bids.ts @@ -56,13 +56,14 @@ export class OFAOrder { } /** Encodes this bid as a ConfidentialComputeRequest, which can be sent to SUAVE. */ - toConfidentialRequest(): TransactionRequestSuave { + toConfidentialRequest(isEIP712?: boolean): TransactionRequestSuave { return { to: this.OFAContract, data: this.newOrderCalldata(), type: '0x43', gas: 500000n, gasPrice: 1000000000n, + isEIP712, kettleAddress: this.kettle, confidentialInputs: this.confidentialInputsBytes(), } diff --git a/examples/suave/index.ts b/examples/suave/index.ts index 49e3f4ea..37f09afa 100644 --- a/examples/suave/index.ts +++ b/examples/suave/index.ts @@ -3,10 +3,9 @@ import { http, Address, Hex, createPublicClient, formatEther, isHex } from 'viem import { goerli } from 'viem/chains' import { TransactionRequestSuave } from 'viem/chains/suave/types' import { OFAOrder } from './bids' -import { SuaveProvider, SuaveWallet, getSuaveProvider, getSuaveWallet } from 'viem/chains/utils' +import { SuaveProvider, SuaveWallet, getSuaveProvider, getSuaveWallet, parseTransactionSuave } from 'viem/chains/utils' import { HttpTransport } from 'viem' import BidContractDeployment from './deployedAddress.json' -import { parseSignedComputeRequest } from 'viem/chains/suave/parsers' const failEnv = (name: string) => { throw new Error(`missing env var ${name}`) @@ -126,12 +125,12 @@ async function testSuaveBids() { KETTLE_ADDRESS, BID_CONTRACT_ADDRESS, ) - const ccr = bid.toConfidentialRequest() + const ccr = bid.toConfidentialRequest() // signs w/ EIP712 by default; pass `false` to use legacy CCR console.log('ccr', ccr) const signedCcr = await wallet.signTransaction(ccr) - const deserCcr = await parseSignedComputeRequest(signedCcr) console.log("signedCcr", signedCcr) + const deserCcr = await parseTransactionSuave(signedCcr) console.log("deserialized signed ccr", deserCcr) // deserCcr should be the same as ccr, but with any missing fields filled in, such as gasPrice & nonce diff --git a/src/chains/suave/parsers.test.ts b/src/chains/suave/parsers.test.ts index 69b5db94..fdee6ccd 100644 --- a/src/chains/suave/parsers.test.ts +++ b/src/chains/suave/parsers.test.ts @@ -27,6 +27,7 @@ describe('Suave Transaction Parsers', () => { gas: 100n, gasPrice: 100n, nonce: 0, + // isEIP712: true, type: SuaveTxRequestTypes.ConfidentialRequest, kettleAddress: accounts[1].address, confidentialInputs: '0x42424242424242424242424242424242' as Hex, @@ -42,10 +43,11 @@ describe('Suave Transaction Parsers', () => { "data": "${ccRequest.data}", "gas": 100n, "gasPrice": 100n, + "isEIP712": true, "kettleAddress": "${ccRequest.kettleAddress}", "nonce": 0, - "r": "0xaf44e7e1c628554f85a8bba6a6ced571e87f5996f195d5e3c97a8c03d4ee61e1", - "s": "0x779bcc8d321f21118ce7682028fcac78123a1d2fcbc40b18866b54ac343c2cf8", + "r": "0xae3d9c09d56078e22d4b9747c988f71d086bb24ab42dc15ea431b7bd10e67a99", + "s": "0x6976c6aaaddec226a56ccb0001935397c68d16f58945769fd855121b7e542863", "to": "${ccRequest.to}", "type": "0x43", "v": 0n, @@ -58,7 +60,7 @@ describe('Suave Transaction Parsers', () => { test('parseTransactionSuave parses all SUAVE tx types', async () => { const wallet = getWallet() const serializedTx = - '0x43f902aaf90184098412504db4830f4240949a151aa453329f3cdf04d8e4e81585a423f7fc2580b8e4d8f55db90000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c012e8eff6ead85d9d948631a18c41afb60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009a151aa453329f3cdf04d8e4e81585a423f7fc25000000000000000000000000000000000000000000000000000000000000000094b5feafbdd752ad52afb7e1bd2e40432a485bbb7fa0249c92db3766bc250ffe17682d363e78dbd3aa1fff59a3b5ca242c872910effa8401008c4580a04a0e49a3711af960c5e76d10a21ae318912702b4cfdb37e6baf087edc84feedca02304c28a2a6cb07efa0643e4e2a78bdd2980ccc1d23b359c9cc67543461eb98ab90120000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000dc7b22747873223a5b2230786638363538303064383235336163393431633638353738353161333737633866613736343130396435353933383261393235376334393962383230336538383038343032303131386164613037613861313734613333643136353432363938616538353061303965303530333262373865353934616164613061343164313137376136383333636266633630613031633465663334313031626161363665376338393438376365353062343239653138623733663535323064366130656633396630366234386362343862373064225d7d00000000' + '0x43f8acf8998064649470997970c51812dc3a010c7d01b50e0d17dc79c880139470997970c51812dc3a010c7d01b50e0d17dc79c8a0a71e488c022df32f3b11c11282cf8c6f6b1d7c1d8b3bc3dc921cb6c2c5c0aae7018401008c4580a0ae3d9c09d56078e22d4b9747c988f71d086bb24ab42dc15ea431b7bd10e67a99a06976c6aaaddec226a56ccb0001935397c68d16f58945769fd855121b7e5428639042424242424242424242424242424242' const parsedTx = parseTransactionSuave(serializedTx) expect(parsedTx.type).toBe(SuaveTxRequestTypes.ConfidentialRequest) diff --git a/src/chains/suave/parsers.ts b/src/chains/suave/parsers.ts index 3f79350a..05834e77 100644 --- a/src/chains/suave/parsers.ts +++ b/src/chains/suave/parsers.ts @@ -25,7 +25,9 @@ const safeHexToNumber = (hex: Hex) => { return hexToNumber(hex) } -export const parseSignedComputeRequest = (signedComputeRequest: Hex) => { +export const parseSignedComputeRequest = ( + signedComputeRequest: Hex, +): Partial => { const serializedType = signedComputeRequest.slice(0, 4) if (serializedType !== SuaveTxRequestTypes.ConfidentialRequest) { throw new InvalidSerializedTransactionTypeError({ @@ -43,6 +45,7 @@ export const parseSignedComputeRequest = (signedComputeRequest: Hex) => { data, kettleAddress, confidentialInputsHash, + isEIP712, chainId, v, r, @@ -50,7 +53,7 @@ export const parseSignedComputeRequest = (signedComputeRequest: Hex) => { ], confidentialInputs, ] = txArray - if (txArray.length !== 2 || txArray[0].length !== 12) { + if (txArray.length !== 2 || txArray[0].length !== 13) { throw new InvalidSerializedTransactionError({ attributes: { nonce, @@ -59,6 +62,7 @@ export const parseSignedComputeRequest = (signedComputeRequest: Hex) => { gas, kettleAddress, confidentialInputsHash, + isEIP712, value, gasPrice, chainId, @@ -78,6 +82,7 @@ export const parseSignedComputeRequest = (signedComputeRequest: Hex) => { gas: hexToBigInt(gas as Hex), kettleAddress: kettleAddress as Hex, confidentialInputs: confidentialInputs as Hex, + isEIP712: isEIP712 === '0x01', value: safeHexToBigInt(value as Hex), gasPrice: safeHexToBigInt(gasPrice as Hex), chainId: hexToNumber(chainId as Hex), @@ -100,7 +105,7 @@ export type ParseTransactionSuaveReturnType = /** Parse a serialized transaction into a SUAVE Transaction object. */ export function parseTransactionSuave( - serializedTransaction: TransactionSerializedSuave, + serializedTransaction: TransactionSerializedSuave | Hex, ): ParseTransactionSuaveReturnType { const serializedType = serializedTransaction.slice(0, 4) const parsedTx = @@ -130,12 +135,19 @@ export function assertTransactionSuave( maxFeePerGas, confidentialInputs, confidentialInputsHash, + isEIP712, kettleAddress, + type, to, r, s, v, } = transaction + if ( + type === SuaveTxRequestTypes.ConfidentialRequest && + isEIP712 === undefined + ) + throw new Error("must encode 'isEIP712' for confidential requests") if (chainId && chainId <= 0) throw new Error('invalid chain ID') if (to && !isAddress(to)) throw new Error('invalid to address') if (!gasPrice) throw new Error('gasPrice is required') diff --git a/src/chains/suave/serializers.ts b/src/chains/suave/serializers.ts index 3d5f42ef..df93a1bb 100644 --- a/src/chains/suave/serializers.ts +++ b/src/chains/suave/serializers.ts @@ -1,7 +1,7 @@ import { InvalidSerializedTransactionTypeError } from '../../index.js' import type { Hex } from '../../types/misc.js' import { concatHex } from '../../utils/data/concat.js' -import { numberToHex, toHex } from '../../utils/encoding/toHex.js' +import { boolToHex, numberToHex, toHex } from '../../utils/encoding/toHex.js' import { toRlp } from '../../utils/encoding/toRlp.js' import { InvalidConfidentialRecordError, @@ -166,6 +166,9 @@ export const serializeConfidentialComputeRequest = ( transaction.kettleAddress, transaction.confidentialInputsHash, + // envelope + boolToHex(transaction.isEIP712 ?? true), + numberToHex(transaction.chainId), toHex(transaction.v), transaction.r, diff --git a/src/chains/suave/types.ts b/src/chains/suave/types.ts index 53181ae7..d15d3fbb 100644 --- a/src/chains/suave/types.ts +++ b/src/chains/suave/types.ts @@ -38,6 +38,7 @@ export type SuaveTxRequestType = type ConfidentialOverrides = { kettleAddress?: Address + isEIP712?: boolean } type ConfidentialComputeRequestOverrides = ConfidentialOverrides & { @@ -136,22 +137,8 @@ export type ConfidentialComputeRecord< TQuantity = bigint, TIndex = number, > = Omit< - Omit< - Omit< - Omit< - TransactionBase< - TQuantity, - TIndex, - SuaveTxTypes.ConfidentialRecord, - TPending - >, - 'blockHash' - >, - 'transactionIndex' - >, - 'blockNumber' - >, - 'from' + TransactionBase, + 'blockHash' | 'transactionIndex' | 'blockNumber' | 'from' > & ConfidentialComputeRecordOverrides @@ -167,6 +154,17 @@ export type TransactionRequestSuave< from?: Address } +export type PreparedConfidentialRecord = Omit< + ConfidentialComputeRecord, + 'input' | 'typeHex' | 'hash' | 'r' | 's' | 'v' +> & { + data: Hex + to: Address + gasPrice: bigint + kettleAddress: Address + confidentialInputsHash: Hash +} + export type RpcTransactionRequestSuave = TransactionRequestSuave & { type: TType @@ -195,7 +193,6 @@ export type TransactionSerializableSuave< > = TransactionSerializableEIP2930 & ConfidentialComputeRecordOverrides & ConfidentialComputeRequestOverrides & { - signedComputeRecord?: Hex type: TType } diff --git a/src/chains/suave/wallet.ts b/src/chains/suave/wallet.ts index 8d681163..4c02bb52 100644 --- a/src/chains/suave/wallet.ts +++ b/src/chains/suave/wallet.ts @@ -1,12 +1,13 @@ +import { sign } from '../../accounts/index.js' import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' -import { sign } from '../../accounts/utils/sign.js' import { http, type JsonRpcAccount, type PrivateKeyAccount, type PublicClient, + TransactionType, type Transport, - type TransportConfig, + TransportConfig, type WalletClient, createPublicClient, createWalletClient, @@ -20,10 +21,12 @@ import { serializeConfidentialComputeRequest, } from './serializers.js' import { + PreparedConfidentialRecord, SuaveTxRequestTypes, + SuaveTxType, SuaveTxTypes, type TransactionRequestSuave, - type TransactionSerializableSuave, + TransactionSerializableSuave, } from './types.js' /// client types @@ -51,6 +54,7 @@ function formatSignature(signature: { } } +/** Sign a CCR with a private key. */ async function signConfidentialComputeRecord( transaction: TransactionSerializableSuave, privateKey: Hex, @@ -68,21 +72,20 @@ async function signConfidentialComputeRecord( } } -/** - * Generates an anonymous function that signs a confidential compute request based on the signing method available to the given `transport` type. - * @param transport The transport to use for signing. - * @param privateKey The private key to use for signing. *Required for **non-custom** transports.* - * @param address The address to use for signing. *Required for **custom** transports.* - * @returns +/** Returns the appropriate function for signing a CCR, as determined by the given transport. + * If the transport is `custom`, `address` must be provided. + * If the transport is not `custom`, `privateKey` must be provided instead. */ function getSigningFunction( transport: TTransport, privateKey?: Hex, address?: Hex, -) { +): ( + txRequest: TransactionSerializableSuave, +) => Promise> { if (transport.type === 'custom') { if (!address) { - throw new Error("param 'address' is required for custom transports") + throw new Error("'address' is required for custom transports") } return async (txRequest: TransactionSerializableSuave) => { const rawSignature: Hex = await transport.request({ @@ -97,10 +100,15 @@ function getSigningFunction( } } else { if (!privateKey) { - throw new Error('privateKey is required for non-custom transports') + throw new Error("'privateKey' is required for non-custom transports") } return async (txRequest: TransactionSerializableSuave) => { - return await signConfidentialComputeRecord(txRequest, privateKey) + const { r, s, v } = await signConfidentialComputeRecord( + txRequest, + privateKey, + ) + if (!r || !s || v === undefined) throw new Error('failed to sign') + return { r, s, v } } } } @@ -144,7 +152,10 @@ export function getSuaveWallet(params: { }) } -async function prepareTx(client: any, txRequest: TransactionRequestSuave) { +async function prepareTx( + client: ReturnType, + txRequest: TransactionRequestSuave, +) { const preparedTx = await client.prepareTransactionRequest(txRequest) const payload: TransactionRequestSuave = { ...txRequest, @@ -169,6 +180,7 @@ function newSuaveWallet(params: { if (params.jsonRpcAccount && params.privateKey) { throw new Error("Cannot provide both 'jsonRpcAccount' and 'privateKey'") } + // Overrides viem wallet methods with SUAVE equivalents. const privateKeyAccount = params.privateKey ? privateKeyToAccount(params.privateKey) @@ -179,6 +191,43 @@ function newSuaveWallet(params: { transport: params.transport, chain: suaveRigil, }).extend((client) => ({ + /** Sign a prepared Confidential Compute Record; like a request, but with `confidentialInputsHash` and `type=0x42` */ + async signEIP712ConfidentialRequest( + request: PreparedConfidentialRecord, + ): Promise> { + if (request.isEIP712 === false) + throw new Error('cannot sign an EIP712 CCR with isEIP712=false') + + const eip712Tx = { + ...request, + nonce: BigInt(request.nonce), + } + const rawSig = await client.signTypedData({ + primaryType: 'ConfidentialRecord', + message: eip712Tx, + types: { + Eip712Domain: [ + { name: 'name', type: 'string' }, + { name: 'verifyingContract', type: 'address' }, + ], + ConfidentialRecord: [ + { name: 'nonce', type: 'uint64' }, + { name: 'gasPrice', type: 'uint256' }, + { name: 'gas', type: 'uint64' }, + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + { name: 'kettleAddress', type: 'address' }, + { name: 'confidentialInputsHash', type: 'bytes32' }, + ], + }, + domain: { + name: 'ConfidentialRecord', + verifyingContract: eip712Tx.kettleAddress, + }, + }) + return hexToSignature(rawSig) + }, async sendTransaction(txRequest: TransactionRequestSuave) { const payload = await prepareTx(client, txRequest) const signedTx = await this.signTransaction(payload) @@ -187,7 +236,9 @@ function newSuaveWallet(params: { params: [signedTx], }) }, - async signTransaction(txRequest: TransactionRequestSuave) { + async signTransaction( + txRequest: TransactionRequestSuave, + ): Promise<`${SuaveTxType | TransactionType}${string}`> { if ( txRequest.type === SuaveTxRequestTypes.ConfidentialRequest || txRequest.kettleAddress || @@ -201,28 +252,69 @@ function newSuaveWallet(params: { if (!txRequest.kettleAddress) { throw new Error('kettleAddress is required for confidential requests') } + if (txRequest.maxFeePerGas || txRequest.maxPriorityFeePerGas) { + throw new Error( + 'maxFeePerGas and maxPriorityFeePerGas are not supported for confidential requests', + ) + } const confidentialInputs = txRequest.confidentialInputs ?? '0x' - // determine signing method based on transport type - const signCcr = getSigningFunction( - client.transport, - params.privateKey, - client.account.address, - ) // get nonce, gas price, etc const ctxParams = prepareTx(client, txRequest) + // dev note: calling (await ...) inline lets us skip the RPC request if teh data is not needed + const nonce = txRequest.nonce ?? (await ctxParams).nonce + const value = txRequest.value ?? 0n + const gas = txRequest.gas ?? (await ctxParams).gas + const gasPrice = txRequest.gasPrice ?? (await ctxParams).gasPrice + const chainId = txRequest.chainId ?? suaveRigil.id + const isEIP712 = txRequest.isEIP712 ?? true + // prepare and sign confidential compute request - const presignTx = { - ...txRequest, - nonce: txRequest.nonce ?? (await ctxParams).nonce, + if (!txRequest.to) { + throw new Error('missing `to`') + } + if (nonce === undefined) { + throw new Error('missing `nonce`') + } + if (gas === undefined) { + throw new Error('missing `gas`') + } + if (gasPrice === undefined) { + throw new Error('missing `gasPrice`') + } + if (!txRequest.kettleAddress) { + throw new Error('missing `kettleAddress`') + } + + const ccRecord: PreparedConfidentialRecord = { + // ...txRequest, + nonce, type: SuaveTxTypes.ConfidentialRecord, + chainId, + to: txRequest.to, + value, + gas, + gasPrice, + data: txRequest.data ?? '0x', + kettleAddress: txRequest.kettleAddress, confidentialInputsHash: keccak256(confidentialInputs), - chainId: txRequest.chainId ?? suaveRigil.id, + isEIP712, } - const sig = await signCcr(presignTx) - const { r, s, v } = sig + + const sig = isEIP712 + ? await this.signEIP712ConfidentialRequest(ccRecord) + : await (async () => { + const signCcr = getSigningFunction( + client.transport, + params.privateKey, + params.jsonRpcAccount?.address, + ) + return await signCcr(ccRecord) + })() + + const { r, s, v } = formatSignature(sig) return serializeConfidentialComputeRequest({ - ...presignTx, + ...ccRecord, confidentialInputs, type: SuaveTxRequestTypes.ConfidentialRequest, r,