diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41e205d7..3d21577f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,9 +6,8 @@ on: # The branches below must be a subset of the branches above branches: [main] jobs: - test: + build: runs-on: ubuntu-latest - if: env.GIT_DIFF steps: - uses: actions/checkout@v2 @@ -25,9 +24,11 @@ jobs: - name: Install modules run: npm install + if: env.GIT_DIFF - name: Build and link packages run: npm run build + if: env.GIT_DIFF - name: Start Docker local node run: >- @@ -36,9 +37,11 @@ jobs: docker create -p 1317:1317 -p 8545:8545 --name evmos -t evmosjs-test && docker start evmos && cd ../../../.. + if: env.GIT_DIFF - name: Wait for Docker to spin up - run: sleep 30 + run: sleep 30s - name: Run tests run: npm run test + if: env.GIT_DIFF diff --git a/README.md b/README.md index 3e0f56e9..52fba4cb 100644 --- a/README.md +++ b/README.md @@ -1,365 +1,17 @@ -# evmosjs +# EvmosJS -JS and TS libs for Evmos. +This is a monorepo that contains the EvmosJS collection of npm packages related to Evmos, +an Ethereum-compatible blockchain platform that supports Ethereum Virtual Machine (EVM) smart contracts. -## Installation +## Packages -evmosjs uses [buf.build](https://buf.build/) to manage [Evmos Protobuf dependencies](https://buf.build/evmos). To install evmosjs packages in your project, -follow the instructions corresponding to your package manager. +This monorepo includes the following packages: -### NPM +- [`@evmos/address-converter`](./packages/address-converter/README.md): JS library for converting between `ETH` and `evmos` addresses. +- [`@evmos/eip712`](./packages/eip712/README.md): JS library for creation of EIP712 transactions. +- [`@evmos/proto`](./packages/proto/README.md): JS library with Protobuf files used to generate cosmos/evmos transactions. +- [`@evmos/provider`](./packages/provider/README.md): JS library to query the EVMOS rest api. +- [`@evmos/transactions`](./packages/transactions/README.md): JS library for constructing, signing, and sending transactions on the Evmos network +- [`evmosjs`](./packages/evmosjs/README.md): JS library containing all the other libraries. -Add the following line to an `.npmrc` file in your project root: - -```ini -@buf:registry=https://buf.build/gen/npm/v1 -``` - -Then run: - -```bash -npm install evmosjs -``` - -Or: - -```bash -npm install @evmos/ -``` - -### Yarn v2.x or v3.x - -Add the following to an `.yarnrc.yml` file in your project root: - -```yaml -npmScopes: - buf: - npmRegistryServer: "https://buf.build/gen/npm/v1" -``` - -Then run: - -```bash -yarn add evmosjs -``` - -Or: - -```bash -yarn add @evmos/ -``` - -Note that Yarn v1 is not supported ([see explanation](https://docs.buf.build/bsr/remote-packages/npm#other-package-managers)). - -## Usage and Examples - -### Query an Account - -Query the account number, sequence, and pubkey for a given address. - -```ts -import { generateEndpointAccount } from '@evmos/provider' - -const address = 'evmos1...' - -// Find node urls for either mainnet or testnet here: -// https://docs.evmos.org/develop/api/networks. -const nodeUrl = '...' -const queryEndpoint = `${nodeUrl}${generateEndpointAccount(address)}` - -const restOptions = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, -} - -// Note that the node will return a 400 status code if the account does not exist. -const rawResult = await fetch( - queryEndpoint, - restOptions, -) - -const result = await rawResult.json() - -// The response format is available at @evmos/provider/rest/account/AccountResponse. -// Note that the `pub_key` will be `null` if the address has not sent any transactions. -/* - account: { - '@type': string - base_account: { - address: string - pub_key?: { - '@type': string - key: string - } - account_number: string - sequence: string - } - code_hash: string - } -*/ -``` - -### Get an Account's Public Key - -Use Keplr or MetaMask to retrieve an account's public key -if it is not returned in the query response. -The public key is necessary in order to sign and broadcast -transactions, and it must be encoded as a compressed key in -`base64`. - -#### Keplr - -```ts -const cosmosChainID = 'evmos_9001-2' // Use 'evmos_9000-4' for testnet - -const account = await window?.keplr?.getKey(cosmosChainID) -const pk = Buffer.from(account.pubKey).toString('base64') -``` - -#### MetaMask - -Since MetaMask does not provide an interface to retrieve a user's -public key, we must sign a message and -[recover the key from a signature](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Public_key_recovery). - -```ts -import { hashMessage } from '@ethersproject/hash' -import { - computePublicKey, - recoverPublicKey, -} from '@ethersproject/signing-key' - -const accounts = await window?.ethereum?.request({ - method: 'eth_requestAccounts', -}) - -// Handle errors if MetaMask fails to return any accounts. -const message = 'Verify Public Key' - -const signature = await window?.ethereum?.request({ - method: 'personal_sign', - params: [message, accounts[0], ''], -}) - -// Compress the key, since the client expects -// public keys to be compressed. -const uncompressedPk = recoverPublicKey( - hashMessage(message), - signature, -) - -const hexPk = computePublicKey(uncompressedPk, true) -const pk = Buffer.from( - hexPk.replace('0x', ''), 'hex', -).toString('base64') -``` - -### Create a Signable Transaction - -Create a transaction payload which can be signed using either Metamask or Keplr. - -This example uses `MsgSend`. View all signable transaction payloads in the [Transaction Docs](https://github.com/evmos/evmosjs/tree/main/docs/transactions). - -```ts -import { - Chain, - Sender, - Fee, - TxContext, - MsgSendParams, - createTxMsgSend, - TxPayload, -} from '@evmos/transactions' - -const chain: Chain = { - chainId: 9001, - cosmosChainId: 'evmos_9001-2', -} - -// Populate the transaction sender parameters using the -// query API. -const sender: Sender = { - accountAddress: , - sequence: , - accountNumber: , - // Use the public key from the account query, or retrieve - // the public key from the code snippet above. - pubkey: , -} - -const fee: Fee = { - amount: '4000000000000000', - denom: 'aevmos', - gas: '200000', -} - -const memo = '' - -const context: TxContext = { - chain, - sender, - fee, - memo, -} - -const params: MsgSendParams = { - destinationAddress: , - amount: , - denom: 'aevmos', -} - -const tx: TxPayload = createTxMsgSend(context, params) -``` - -### Sign the Transaction with MetaMask - -Evmos supports EIP-712 signatures for Cosmos payloads to be signed using Ethereum wallets such as MetaMask. - -```ts -import { createTxRaw } from '@evmos/proto' -import { evmosToEth } from '@evmos/address-converter' - -// First, populate a TxContext object and create a signable Tx payload. -// (See 'Create a Signable Transaction' to learn how to create these). -const context = ... -const tx = ... - -const { sender } = context - -// Initialize MetaMask and sign the EIP-712 payload. -await window.ethereum.enable() - -const senderHexAddress = evmosToEth(sender.accountAddress) -const eip712Payload = JSON.stringify(tx.eipToSign) - -const signature = await window.ethereum.request({ - method: 'eth_signTypedData_v4', - params: [senderHexAddress, eip712Payload], -}) - -// Create a signed Tx payload that can be broadcast to a node. -const signatureBytes = Buffer.from(signature.replace('0x', ''), 'hex') - -const { signDirect } = tx -const bodyBytes = signDirect.body.toBinary() -const authInfoBytes = signDirect.authInfo.toBinary() - -const signedTx = createTxRaw( - bodyBytes, - authInfoBytes, - [signatureBytes], -) -``` - -### Sign the Transaction with Keplr (SignDirect) - -EvmosJS supports Cosmos SDK `SignDirect` payloads that can be signed using Keplr. - -```ts -import { createTxRaw } from '@evmos/proto' - -// First, populate a TxContext object and create a signable Tx payload. -// (See 'Create a Signable Transaction' to learn how to create these). -const context = ... -const tx = ... - -const { chain, sender } = context -const { signDirect } = tx - -const signResponse = await window?.keplr?.signDirect( - chain.cosmosChainId, - sender.accountAddress, - { - bodyBytes: signDirect.body.toBinary(), - authInfoBytes: signDirect.authInfo.toBinary(), - chainId: chain.cosmosChainId, - accountNumber: new Long(sender.accountNumber), - }, -) - -if (!signResponse) { - // Handle signature failure here. -} - -const signatures = [ - new Uint8Array(Buffer.from(signResponse.signature.signature, 'base64')), -] - -const { signed } = signResponse - -const signedTx = createTxRaw( - signed.bodyBytes, - signed.authInfoBytes, - signatures, -) -``` - -### Sign the Transaction with Keplr (EIP-712) - -EvmosJS also supports signing [EIP-712](https://eips.ethereum.org/EIPS/eip-712) payloads using Keplr. This is necessary for Ledger users on Keplr, since the Ledger device cannot sign `SignDirect` payloads. - -```ts -import { EthSignType } from '@keplr-wallet/types'; -import { createTxRaw } from '@evmos/proto' - -// First, populate a TxContext object and create a signable Tx payload. -// (See 'Create a Signable Transaction' to learn how to create these). -const context = ... -const tx = ... - -const { chain, sender } = context - -const eip712Payload = JSON.stringify(tx.eipToSign) -const signature = await window?.keplr?.signEthereum( - chain.cosmosChainId, - sender.accountAddress, - eip712Payload, - EthSignType.EIP712, -) - -if (!signature) { - // Handle signature failure here. -} - -const { signDirect } = tx -const bodyBytes = signDirect.body.toBinary() -const authInfoBytes = signDirect.authInfo.toBinary() - -const signedTx = createTxRaw( - bodyBytes, - authInfoBytes, - [signature], -) -``` - -### Broadcast the Signed Transaction - -Regardless of how the transaction is signed, broadcasting takes place the same way. - -```ts -import { - generateEndpointBroadcast, - generatePostBodyBroadcast, -} from '@evmos/provider' - -// First, sign a transaction using MetaMask or Keplr. -const signedTx = createTxRaw(...) - -// Find a node URL from a network endpoint: -// https://docs.evmos.org/develop/api/networks. -const nodeUrl = ... - -const postOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: generatePostBodyBroadcast(signedTx), -} - -const broadcastEndpoint = `${nodeUrl}${generateEndpointBroadcast()}` -const broadcastPost = await fetch( - broadcastEndpoint, - postOptions, -) - -const response = await broadcastPost.json() -``` +For installation guide and examples, check the [`evmosjs` readme file](./packages/evmosjs/README.md). diff --git a/packages/evmosjs/README.md b/packages/evmosjs/README.md index ad955498..3e0f56e9 100644 --- a/packages/evmosjs/README.md +++ b/packages/evmosjs/README.md @@ -1,3 +1,365 @@ -# Evmosjs +# evmosjs -Main package +JS and TS libs for Evmos. + +## Installation + +evmosjs uses [buf.build](https://buf.build/) to manage [Evmos Protobuf dependencies](https://buf.build/evmos). To install evmosjs packages in your project, +follow the instructions corresponding to your package manager. + +### NPM + +Add the following line to an `.npmrc` file in your project root: + +```ini +@buf:registry=https://buf.build/gen/npm/v1 +``` + +Then run: + +```bash +npm install evmosjs +``` + +Or: + +```bash +npm install @evmos/ +``` + +### Yarn v2.x or v3.x + +Add the following to an `.yarnrc.yml` file in your project root: + +```yaml +npmScopes: + buf: + npmRegistryServer: "https://buf.build/gen/npm/v1" +``` + +Then run: + +```bash +yarn add evmosjs +``` + +Or: + +```bash +yarn add @evmos/ +``` + +Note that Yarn v1 is not supported ([see explanation](https://docs.buf.build/bsr/remote-packages/npm#other-package-managers)). + +## Usage and Examples + +### Query an Account + +Query the account number, sequence, and pubkey for a given address. + +```ts +import { generateEndpointAccount } from '@evmos/provider' + +const address = 'evmos1...' + +// Find node urls for either mainnet or testnet here: +// https://docs.evmos.org/develop/api/networks. +const nodeUrl = '...' +const queryEndpoint = `${nodeUrl}${generateEndpointAccount(address)}` + +const restOptions = { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, +} + +// Note that the node will return a 400 status code if the account does not exist. +const rawResult = await fetch( + queryEndpoint, + restOptions, +) + +const result = await rawResult.json() + +// The response format is available at @evmos/provider/rest/account/AccountResponse. +// Note that the `pub_key` will be `null` if the address has not sent any transactions. +/* + account: { + '@type': string + base_account: { + address: string + pub_key?: { + '@type': string + key: string + } + account_number: string + sequence: string + } + code_hash: string + } +*/ +``` + +### Get an Account's Public Key + +Use Keplr or MetaMask to retrieve an account's public key +if it is not returned in the query response. +The public key is necessary in order to sign and broadcast +transactions, and it must be encoded as a compressed key in +`base64`. + +#### Keplr + +```ts +const cosmosChainID = 'evmos_9001-2' // Use 'evmos_9000-4' for testnet + +const account = await window?.keplr?.getKey(cosmosChainID) +const pk = Buffer.from(account.pubKey).toString('base64') +``` + +#### MetaMask + +Since MetaMask does not provide an interface to retrieve a user's +public key, we must sign a message and +[recover the key from a signature](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Public_key_recovery). + +```ts +import { hashMessage } from '@ethersproject/hash' +import { + computePublicKey, + recoverPublicKey, +} from '@ethersproject/signing-key' + +const accounts = await window?.ethereum?.request({ + method: 'eth_requestAccounts', +}) + +// Handle errors if MetaMask fails to return any accounts. +const message = 'Verify Public Key' + +const signature = await window?.ethereum?.request({ + method: 'personal_sign', + params: [message, accounts[0], ''], +}) + +// Compress the key, since the client expects +// public keys to be compressed. +const uncompressedPk = recoverPublicKey( + hashMessage(message), + signature, +) + +const hexPk = computePublicKey(uncompressedPk, true) +const pk = Buffer.from( + hexPk.replace('0x', ''), 'hex', +).toString('base64') +``` + +### Create a Signable Transaction + +Create a transaction payload which can be signed using either Metamask or Keplr. + +This example uses `MsgSend`. View all signable transaction payloads in the [Transaction Docs](https://github.com/evmos/evmosjs/tree/main/docs/transactions). + +```ts +import { + Chain, + Sender, + Fee, + TxContext, + MsgSendParams, + createTxMsgSend, + TxPayload, +} from '@evmos/transactions' + +const chain: Chain = { + chainId: 9001, + cosmosChainId: 'evmos_9001-2', +} + +// Populate the transaction sender parameters using the +// query API. +const sender: Sender = { + accountAddress: , + sequence: , + accountNumber: , + // Use the public key from the account query, or retrieve + // the public key from the code snippet above. + pubkey: , +} + +const fee: Fee = { + amount: '4000000000000000', + denom: 'aevmos', + gas: '200000', +} + +const memo = '' + +const context: TxContext = { + chain, + sender, + fee, + memo, +} + +const params: MsgSendParams = { + destinationAddress: , + amount: , + denom: 'aevmos', +} + +const tx: TxPayload = createTxMsgSend(context, params) +``` + +### Sign the Transaction with MetaMask + +Evmos supports EIP-712 signatures for Cosmos payloads to be signed using Ethereum wallets such as MetaMask. + +```ts +import { createTxRaw } from '@evmos/proto' +import { evmosToEth } from '@evmos/address-converter' + +// First, populate a TxContext object and create a signable Tx payload. +// (See 'Create a Signable Transaction' to learn how to create these). +const context = ... +const tx = ... + +const { sender } = context + +// Initialize MetaMask and sign the EIP-712 payload. +await window.ethereum.enable() + +const senderHexAddress = evmosToEth(sender.accountAddress) +const eip712Payload = JSON.stringify(tx.eipToSign) + +const signature = await window.ethereum.request({ + method: 'eth_signTypedData_v4', + params: [senderHexAddress, eip712Payload], +}) + +// Create a signed Tx payload that can be broadcast to a node. +const signatureBytes = Buffer.from(signature.replace('0x', ''), 'hex') + +const { signDirect } = tx +const bodyBytes = signDirect.body.toBinary() +const authInfoBytes = signDirect.authInfo.toBinary() + +const signedTx = createTxRaw( + bodyBytes, + authInfoBytes, + [signatureBytes], +) +``` + +### Sign the Transaction with Keplr (SignDirect) + +EvmosJS supports Cosmos SDK `SignDirect` payloads that can be signed using Keplr. + +```ts +import { createTxRaw } from '@evmos/proto' + +// First, populate a TxContext object and create a signable Tx payload. +// (See 'Create a Signable Transaction' to learn how to create these). +const context = ... +const tx = ... + +const { chain, sender } = context +const { signDirect } = tx + +const signResponse = await window?.keplr?.signDirect( + chain.cosmosChainId, + sender.accountAddress, + { + bodyBytes: signDirect.body.toBinary(), + authInfoBytes: signDirect.authInfo.toBinary(), + chainId: chain.cosmosChainId, + accountNumber: new Long(sender.accountNumber), + }, +) + +if (!signResponse) { + // Handle signature failure here. +} + +const signatures = [ + new Uint8Array(Buffer.from(signResponse.signature.signature, 'base64')), +] + +const { signed } = signResponse + +const signedTx = createTxRaw( + signed.bodyBytes, + signed.authInfoBytes, + signatures, +) +``` + +### Sign the Transaction with Keplr (EIP-712) + +EvmosJS also supports signing [EIP-712](https://eips.ethereum.org/EIPS/eip-712) payloads using Keplr. This is necessary for Ledger users on Keplr, since the Ledger device cannot sign `SignDirect` payloads. + +```ts +import { EthSignType } from '@keplr-wallet/types'; +import { createTxRaw } from '@evmos/proto' + +// First, populate a TxContext object and create a signable Tx payload. +// (See 'Create a Signable Transaction' to learn how to create these). +const context = ... +const tx = ... + +const { chain, sender } = context + +const eip712Payload = JSON.stringify(tx.eipToSign) +const signature = await window?.keplr?.signEthereum( + chain.cosmosChainId, + sender.accountAddress, + eip712Payload, + EthSignType.EIP712, +) + +if (!signature) { + // Handle signature failure here. +} + +const { signDirect } = tx +const bodyBytes = signDirect.body.toBinary() +const authInfoBytes = signDirect.authInfo.toBinary() + +const signedTx = createTxRaw( + bodyBytes, + authInfoBytes, + [signature], +) +``` + +### Broadcast the Signed Transaction + +Regardless of how the transaction is signed, broadcasting takes place the same way. + +```ts +import { + generateEndpointBroadcast, + generatePostBodyBroadcast, +} from '@evmos/provider' + +// First, sign a transaction using MetaMask or Keplr. +const signedTx = createTxRaw(...) + +// Find a node URL from a network endpoint: +// https://docs.evmos.org/develop/api/networks. +const nodeUrl = ... + +const postOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: generatePostBodyBroadcast(signedTx), +} + +const broadcastEndpoint = `${nodeUrl}${generateEndpointBroadcast()}` +const broadcastPost = await fetch( + broadcastEndpoint, + postOptions, +) + +const response = await broadcastPost.json() +``` diff --git a/packages/evmosjs/package.json b/packages/evmosjs/package.json index f63f665d..7ec01015 100644 --- a/packages/evmosjs/package.json +++ b/packages/evmosjs/package.json @@ -1,7 +1,7 @@ { "name": "evmosjs", "description": "JS and TS libs for Evmos", - "version": "0.3.3", + "version": "0.4.0", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/evmosjs/src/tests/Dockerfile b/packages/evmosjs/src/tests/Dockerfile index a25946df..2849bfb0 100644 --- a/packages/evmosjs/src/tests/Dockerfile +++ b/packages/evmosjs/src/tests/Dockerfile @@ -1,16 +1,4 @@ -FROM golang:1.20.0-bullseye AS build-env - -WORKDIR /go/src/github.com/evmos/ - -RUN git clone "https://github.com/evmos/evmos.git" - -WORKDIR /go/src/github.com/evmos/evmos - -RUN apt-get update -y - -RUN make build - -FROM golang:1.20.0-bullseye +FROM tharsishq/evmos:latest RUN apt-get update \ && apt-get install jq=1.6-2.1 -y --no-install-recommends \ @@ -24,8 +12,6 @@ COPY ./init-node.sh . # perms to make init-node.sh executable RUN chmod +x init-node.sh -COPY --from=build-env /go/src/github.com/evmos/evmos/build/evmosd /usr/bin/evmosd - EXPOSE 1317 8545 26657 CMD ["sh", "./init-node.sh"]