diff --git a/.changeset/afraid-planets-beam.md b/.changeset/afraid-planets-beam.md new file mode 100644 index 00000000..824d0950 --- /dev/null +++ b/.changeset/afraid-planets-beam.md @@ -0,0 +1,5 @@ +--- +"@onflow/flow-js-testing": minor +--- + +Flow JS Testing now exports [`SignatureAlgorithm`](/docs/api.md#signaturealgorithm) and [`HashAlgorithm`](/docs/api.md#hashalgorithm) which are enums that may be used with [`createAccount`](/docs/accounts.md#createaccountname-keys) and [`sendTransaction`](/docs/send-transactions.md) diff --git a/.changeset/clean-oranges-hammer.md b/.changeset/clean-oranges-hammer.md index ae77f81d..2a57217e 100644 --- a/.changeset/clean-oranges-hammer.md +++ b/.changeset/clean-oranges-hammer.md @@ -2,4 +2,4 @@ "@onflow/flow-js-testing": minor --- -Allow custom transaction signers to be provided as object with `addr`, `privateKey`, `keyId`, `hashAlgorithm` keys as an alternative to supplying merely the signer's account address and having Flow JS Testing determine the rest. This allows for more complex transaction authorizers. See [documentation for examples](/docs/send-transactions.md). +Allow custom transaction signers to be provided as object with `addr`, `privateKey`, `keyId`, `hashAlgorithm`, `signatureAlgorithm` keys as an alternative to supplying merely the signer's account address and having Flow JS Testing determine the rest. This allows for more complex transaction authorizers. See [documentation for examples](/docs/send-transactions.md). diff --git a/.changeset/kind-fishes-switch.md b/.changeset/kind-fishes-switch.md new file mode 100644 index 00000000..7ba8df2b --- /dev/null +++ b/.changeset/kind-fishes-switch.md @@ -0,0 +1,5 @@ +--- +"@onflow/flow-js-testing": minor +--- + +Flow JS Testing now exports the [`pubFlowKey`](/docs/api.md#pubflowkeykeyobject) method which may be used to generate an RLP-encoded `Buffer` representing a public key corresponding to a particular private key. diff --git a/.changeset/pre.json b/.changeset/pre.json index a5ba84ee..056ca7ba 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -5,7 +5,6 @@ "@onflow/flow-js-testing": "0.2.3-alpha.7" }, "changesets": [ - "clean-oranges-hammer", "giant-dots-trade", "gold-cheetahs-attend", "long-hairs-collect", diff --git a/.changeset/tasty-fans-repeat.md b/.changeset/tasty-fans-repeat.md new file mode 100644 index 00000000..ebf906e2 --- /dev/null +++ b/.changeset/tasty-fans-repeat.md @@ -0,0 +1,5 @@ +--- +"@onflow/flow-js-testing": minor +--- + +Flow JS Testing now exports the [`createAccount`](/docs/accounts.md#createaccountname-keys) method which may be used to manually create an account with a given human-readable name & specified keys. diff --git a/CHANGELOG.md b/CHANGELOG.md index 60557ab2..f8bb1c45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,5 @@ # flow-js-testing -## 0.3.0-alpha.13 - -### Minor Changes - -- [#155](https://github.com/onflow/flow-js-testing/pull/155) [`9dcab53`](https://github.com/onflow/flow-js-testing/commit/9dcab535393654e3c6ba41a3ac41095519446c27) Thanks [@jribbink](https://github.com/jribbink)! - Allow custom transaction signers to be provided as object with `addr`, `privateKey`, `keyId`, `hashAlgorithm` keys as an alternative to supplying merely the signer's account address and having Flow JS Testing determine the rest. This allows for more complex transaction authorizers. See [documentation for examples](/docs/send-transactions.md). - ## 0.3.0-alpha.12 ### Patch Changes diff --git a/cadence/transactions/create-account.cdc b/cadence/transactions/create-account.cdc index 24085fa9..e01186a8 100644 --- a/cadence/transactions/create-account.cdc +++ b/cadence/transactions/create-account.cdc @@ -1,17 +1,21 @@ import FlowManager from 0x01 -transaction (_ name: String, pubKey: String, manager: Address) { +transaction (_ name: String?, pubKey: [String], manager: Address) { prepare( admin: AuthAccount) { let newAccount = AuthAccount(payer:admin) - newAccount.addPublicKey(pubKey.decodeHex()) + for key in pubKey { + newAccount.addPublicKey(key.decodeHex()) + } - let linkPath = FlowManager.accountManagerPath - let accountManager = getAccount(manager) - .getCapability(linkPath)! - .borrow<&FlowManager.Mapper>()! - - // Create a record in account database - let address = newAccount.address - accountManager.setAddress(name, address: address) + if name != nil { + let linkPath = FlowManager.accountManagerPath + let accountManager = getAccount(manager) + .getCapability(linkPath)! + .borrow<&FlowManager.Mapper>()! + + // Create a record in account database + let address = newAccount.address + accountManager.setAddress(name!, address: address) + } } } diff --git a/dev-test/account.test.js b/dev-test/account.test.js new file mode 100644 index 00000000..d2524793 --- /dev/null +++ b/dev-test/account.test.js @@ -0,0 +1,146 @@ +import path from "path" +import { + emulator, + init, + getAccountAddress, + executeScript, + createAccount, + HashAlgorithm, + SignatureAlgorithm, +} from "../src" +import {playgroundImport} from "../src/transformers" +import * as fcl from "@onflow/fcl" +import {validateKeyPair} from "./util/validate-key-pair" +import {permute} from "./util/permute" +import {isAddress} from "../src" + +beforeEach(async () => { + const basePath = path.resolve(__dirname, "./cadence") + await init(basePath) + return emulator.start() +}) + +afterEach(async () => { + emulator.stop() +}) + +it("createAccount - should work with name and resolves to correct getAccountAddress", async () => { + const addr1 = await createAccount({ + name: "Billy", + }) + + const addr2 = await getAccountAddress("Billy") + + expect(addr1).toBe(addr2) +}) + +it("createAccount - should work without name and returns address", async () => { + const Billy = await createAccount({ + name: "Billy", + }) + + expect(isAddress(Billy)).toBe(true) +}) + +test.each(permute(Object.keys(HashAlgorithm), Object.keys(SignatureAlgorithm)))( + "createAccount - should work with custom keys - hash algorithm %s - signing algorithm %s", + async (hashAlgorithm, signatureAlgorithm) => { + const privateKey = "1234" + const Billy = await createAccount({ + name: "Billy", + keys: [ + { + privateKey, + hashAlgorithm: HashAlgorithm[hashAlgorithm], + signatureAlgorithm: SignatureAlgorithm[signatureAlgorithm], + }, + ], + }) + + expect(isAddress(Billy)).toBe(true) + + const keys = await fcl.account(Billy).then(d => d.keys) + + expect(keys.length).toBe(1) + expect(keys[0].hashAlgoString).toBe(hashAlgorithm) + expect(keys[0].signAlgoString).toBe(signatureAlgorithm) + expect(keys[0].weight).toBe(1000) + expect( + validateKeyPair(keys[0].publicKey, privateKey, signatureAlgorithm) + ).toBe(true) + } +) + +test("createAccount - should work with custom keys - defaults to SHA3_256, ECDSA_P256", async () => { + const privateKey = "1234" + const Billy = await createAccount({ + name: "Billy", + keys: [ + { + privateKey, + }, + ], + }) + + expect(isAddress(Billy)).toBe(true) + + const keys = await fcl.account(Billy).then(d => d.keys) + + expect(keys.length).toBe(1) + expect(keys[0].hashAlgoString).toBe("SHA3_256") + expect(keys[0].signAlgoString).toBe("ECDSA_P256") + expect(keys[0].weight).toBe(1000) + expect( + validateKeyPair( + keys[0].publicKey, + privateKey, + SignatureAlgorithm.ECDSA_P256 + ) + ).toBe(true) +}) + +it("createAccount - should add universal private key to account by default", async () => { + const Billy = await createAccount({ + name: "Billy", + }) + + expect(isAddress(Billy)).toBe(true) +}) + +it("getAccountAddress - should return proper playground addresses", async () => { + const accounts = ["Alice", "Bob", "Charlie", "Dave", "Eve"] + for (const i in accounts) { + await getAccountAddress(accounts[i]) + } + + const code = ` + pub fun main(address:Address):Address{ + return getAccount(address).address + } + ` + + const playgroundAddresses = ["0x01", "0x02", "0x03", "0x04", "0x05"] + for (const i in playgroundAddresses) { + const [result] = await executeScript({ + code, + transformers: [playgroundImport(accounts)], + args: [playgroundAddresses[i]], + }) + const account = await getAccountAddress(accounts[i]) + expect(result).toBe(account) + } +}) + +it("getAccountAddress - should create an account if does not exist", async () => { + const Billy = await getAccountAddress("Billy") + + expect(isAddress(Billy)).toBe(true) +}) + +it("getAccountAddress - should resolve an already created account", async () => { + const Billy1 = await getAccountAddress("Billy") + const Billy2 = await getAccountAddress("Billy") + + expect(isAddress(Billy1)).toBe(true) + expect(Billy1).toMatch(Billy2) +}) diff --git a/dev-test/interaction.test.js b/dev-test/interaction.test.js index 60291dc4..38a5921e 100644 --- a/dev-test/interaction.test.js +++ b/dev-test/interaction.test.js @@ -11,6 +11,9 @@ import { shallThrow, shallPass, } from "../src" +import {createAccount} from "../src/account" +import {HashAlgorithm, pubFlowKey, SignatureAlgorithm} from "../src/crypto" +import {permute} from "./util/permute" // We need to set timeout for a higher number, cause some transactions might take up some time jest.setTimeout(10000) @@ -116,6 +119,82 @@ describe("interactions - sendTransaction", () => { await shallPass(sendTransaction({code, signers})) }) + test.each( + permute(Object.keys(HashAlgorithm), Object.keys(SignatureAlgorithm)) + )( + "sendTransaction - shall pass with custom signer - %s hash algorithm, %s signature algorithm", + async (hashAlgorithm, signatureAlgorithm) => { + const privateKey = "1234" + const Adam = await createAccount({ + name: "Adam", + keys: [ + await pubFlowKey({ + privateKey, + hashAlgorithm, + signatureAlgorithm, + weight: 1000, + }), + ], + }) + + const code = ` + transaction{ + prepare(signer: AuthAccount){ + assert(signer.address == ${Adam}, message: "Signer address must be equal to Adam's Address") + } + } + ` + const signers = [ + { + addr: Adam, + keyId: 0, + privateKey, + hashAlgorithm, + signatureAlgorithm, + }, + ] + + await shallPass(sendTransaction({code, signers})) + } + ) + + test("sendTransaction - shall pass with custom signer - hashAlgorithm, signatureAlgorithm resolved via string - pubKey resolved via privKey", async () => { + const hashAlgorithm = "ShA3_256" //varying caps to test case insensitivity + const signatureAlgorithm = "eCdSA_P256" + + const privateKey = "1234" + const Adam = await createAccount({ + name: "Adam", + keys: [ + { + privateKey, + hashAlgorithm, + signatureAlgorithm, + weight: 1000, + }, + ], + }) + + const code = ` + transaction{ + prepare(signer: AuthAccount){ + assert(signer.address == ${Adam}, message: "Signer address must be equal to Adam's Address") + } + } + ` + const signers = [ + { + addr: Adam, + keyId: 0, + privateKey, + hashAlgorithm, + signatureAlgorithm, + }, + ] + + await shallPass(sendTransaction({code, signers})) + }) + test("sendTransaction - argument mapper - basic", async () => { await shallPass(async () => { const code = ` diff --git a/dev-test/util/permute.js b/dev-test/util/permute.js new file mode 100644 index 00000000..5d6800dc --- /dev/null +++ b/dev-test/util/permute.js @@ -0,0 +1,8 @@ +export const permute = (...values) => + values.length > 1 + ? permute( + values[0].reduce((acc, i) => { + return [...acc, ...values[1].map(j => [].concat(i).concat(j))] + }, []) + ) + : values[0] diff --git a/dev-test/util/validate-key-pair.js b/dev-test/util/validate-key-pair.js new file mode 100644 index 00000000..06e32a35 --- /dev/null +++ b/dev-test/util/validate-key-pair.js @@ -0,0 +1,33 @@ +import {ec as EC} from "elliptic" +import { + resolveSignAlgoKey, + SignAlgoECMap, + SignatureAlgorithm, +} from "../../src/crypto" +import {isString} from "../../src/utils" + +export function validateKeyPair( + publicKey, + privateKey, + signatureAlgorithm = SignatureAlgorithm.P256 +) { + const signAlgoKey = resolveSignAlgoKey(signatureAlgorithm) + const curve = SignAlgoECMap[signAlgoKey] + + const prepareKey = key => { + if (isString(key)) key = Buffer.from(key, "hex") + if (key.at(0) !== 0x04) key = Buffer.concat([Buffer.from([0x04]), key]) + return key + } + + publicKey = prepareKey(publicKey) + privateKey = prepareKey(privateKey) + + const ec = new EC(curve) + const pair = ec.keyPair({ + pub: publicKey, + priv: privateKey, + }) + + return pair.validate()?.result ?? false +} diff --git a/dev-test/utilities.test.js b/dev-test/utilities.test.js index e06917a2..21423765 100644 --- a/dev-test/utilities.test.js +++ b/dev-test/utilities.test.js @@ -4,7 +4,6 @@ import { emulator, init, getServiceAddress, - getAccountAddress, shallPass, shallResolve, executeScript, @@ -20,7 +19,7 @@ import { builtInMethods, playgroundImport, } from "../src/transformers" -import {getManagerAddress, initManager} from "../src/manager" +import {getManagerAddress} from "../src/manager" import * as manager from "../src/manager" // We need to set timeout for a higher number, cause some transactions might take up some time @@ -235,31 +234,6 @@ describe("dev tests", () => { const [newOffset] = await executeScript("get-block-offset") expect(newOffset).toBe(String(offset)) }) - - it("should return proper addresses", async () => { - await initManager() - const accounts = ["Alice", "Bob", "Charlie", "Dave", "Eve"] - for (const i in accounts) { - await getAccountAddress(accounts[i]) - } - - const code = ` - pub fun main(address:Address):Address{ - return getAccount(address).address - } - ` - - const playgroundAddresses = ["0x01", "0x02", "0x03", "0x04", "0x05"] - for (const i in playgroundAddresses) { - const [result] = await executeScript({ - code, - transformers: [playgroundImport(accounts)], - args: [playgroundAddresses[i]], - }) - const account = await getAccountAddress(accounts[i]) - expect(result).toBe(account) - } - }) }) describe("transformers and injectors", () => { diff --git a/docs/accounts.md b/docs/accounts.md index 23eca9d7..4e518833 100644 --- a/docs/accounts.md +++ b/docs/accounts.md @@ -4,11 +4,25 @@ sidebar_title: Accounts description: How to manage accounts addresses --- +## Overview + Flow accounts are not derived from a private key. This creates an issues for testing, since you need to create actors in a specific order to use their addresses properly. -In order to reduce this friction we made a handy method `getAccountAddress`, which allows you to access specific address using an alias. This way you can think about actual actors - for example `Alice` and `Bob` - without needing to know their Flow addresses. + +To reduce this friction, `getAccountAddress`, allows you to access a specific address using an alias. This way you can think about actual actors -- for example `Alice` and `Bob` -- without needing to know their Flow addresses. + It also helps you to write tests in a sequential or non-sequential way. Calling this method for the first time will create a new account and return the address. Calling it a second time with the same alias again will return the Flow address for that account, without creating new account. +## Private Key Management + +#### Universal private key + +By default, accounts created and consumed by the Flow JS Testing library will use a universal private key for signing transactions. Generally, this alleviates the burden of any low-level key management and streamlines the process of testing cadence code. + +#### Custom private keys + +However, under some circumstances the user may wish to create accounts (see: [`createAccount`](#createaccountname-keys)) or sign for accounts (see: [`sendTransaction`](./send-transactions.md)) using custom private keys (i.e. private key value, [hashing algorithm](./api.md#hashalgorithm), [signing algorithm](./send-transactions.md#signaturealgorithm), etc.) - this functionality is facilitated by the aforementioned methods. + ## `getAccountAddress` Resolves name alias to a Flow address (`0x` prefixed) under the following conditions: @@ -40,3 +54,24 @@ const main = async () => { main() ``` + +## `createAccount({name, keys})` + +In some cases, you may wish to manually create an account with a particular set of private keys + +#### Options + +_Pass in the following as a single object with the following keys._ + +| Key | Type | Required | Description | +| ------ | -------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `name` | string | No | human-readable name to be associated with created account (will be used for address lookup within [getAccountAddress](#getaccountaddress)) | +| `keys` | [[KeyObject](./api.md#keyobject) or [PublicKey](./api.md#publickey)] | No | An array of [KeyObjects](#./api.md#keyobject) or [PublicKeys](./api.md#publickey) to be added to the account upon creation (defaults to the [universal private key](./accounts#universal-private-key)) | + +> 📣 if `name` field not provided, the account address will not be cached and you will be unable to look it up using [`getAccountAddress`](#getaccountaddress). + +#### Returns + +| Type | Description | +| ------------------------------------------------------------- | ---------------------------------------- | +| [Address](https://docs.onflow.org/fcl/reference/api/#address) | `0x` prefixed address of created account | \ No newline at end of file diff --git a/docs/api.md b/docs/api.md index 2c8df9b3..6fbf429f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,8 +8,8 @@ Resolves name alias to a Flow address (`0x` prefixed) under the following conditions: -- If account with specific name has not been previously accessed framework will first create a new one and then store it under provided alias. -- Next time when you call this method, it will grab exactly the same account. This allows you to create several accounts first and then use them throughout your code, without worrying that accounts match or trying to store/handle specific addresses. +- If an account with a specific name has not been previously accessed, the framework will create a new one and then store it under the provided alias. +- Next time when you call this method, it will grab exactly the same account. This allows you to create several accounts up-front and then use them throughout your code, without worrying that accounts match or trying to store and manage specific addresses. #### Arguments @@ -26,15 +26,9 @@ Resolves name alias to a Flow address (`0x` prefixed) under the following condit #### Usage ```javascript -import path from "path" -import {init, emulator, getAccountAddress} from "@onflow/flow-js-testing" +import {getAccountAddress} from "@onflow/flow-js-testing" const main = async () => { - const basePath = path.resolve(__dirname, "../cadence") - - await init(basePath) - await emulator.start() - const Alice = await getAccountAddress("Alice") console.log({Alice}) } @@ -42,6 +36,27 @@ const main = async () => { main() ``` +### `createAccount({name, keys})` + +In some cases, you may wish to manually create an account with a particular set of private keys + +#### Options + +_Pass in the following as a single object with the following keys._ + +| Key | Type | Required | Description | +| ------ | -------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `name` | string | No | human-readable name to be associated with created account (will be used for address lookup within [getAccountAddress](#getaccountaddress)) | +| `keys` | [[KeyObject](./api.md#keyobject) or [PublicKey](./api.md#publickey)] | No | An array of [KeyObjects](#./api.md#keyobject) or [PublicKeys](./api.md#publickey) to be added to the account upon creation (defaults to the [universal private key](./accounts#universal-private-key)) | + +> 📣 if `name` field not provided, the account address will not be cached and you will be unable to look it up using [`getAccountAddress`](#getaccountaddress). + +#### Returns + +| Type | Description | +| ------------------------------------------------------------- | ---------------------------------------- | +| [Address](https://docs.onflow.org/fcl/reference/api/#address) | `0x` prefixed address of created account | + ## Contracts ### `deployContractByName(props)` @@ -135,7 +150,6 @@ import { deployContract, executeScript, } from "@onflow/flow-js-testing" - ;(async () => { const basePath = path.resolve(__dirname, "../cadence") @@ -196,7 +210,6 @@ Returns address of the account where the contract is currently deployed. ```javascript import path from "path" import {init, emulator, deployContractByName, getContractAddress} from "../src" - ;(async () => { const basePath = path.resolve(__dirname, "./cadence") @@ -217,6 +230,18 @@ import {init, emulator, deployContractByName, getContractAddress} from "../src" 📣 Framework does not support contracts with identical names deployed to different accounts. While you can deploy contract to a new address, the internal system, which tracks where contracts are deployed, will only store last address. +## Cryptography + +### `pubFlowKey(keyObject)` + +The `pubFlowKey` method exported by Flow JS Testing Library will generate an RLP-encoded public key given a private key, hashing algorithm, signing algorithm, and key weight. + +| Name | Type | Optional | Description | +| ----------- | ----------------------- | -------- | -------------------------------------------------------------------------- | +| `keyObject` | [KeyObject](#keyobject) | ✅ | an object containing a private key & the key's hashing/signing information | + +If `keyObject` is not provided, Flow JS Testing will default to the [universal private key](./accounts.md#universal-private-key). + ## Emulator Flow Javascript Testing Framework exposes `emulator` singleton allowing you to run and stop emulator instance @@ -253,7 +278,6 @@ Starts emulator on a specified port. Returns Promise. ```javascript import path from "path" import {emulator, init} from "../src" - ;(async () => { const basePath = path.resolve(__dirname, "../cadence") @@ -431,7 +455,6 @@ import { getFlowBalance, mintFlow, } from "../src" - ;(async () => { const basePath = path.resolve(__dirname, "./cadence") @@ -509,8 +532,8 @@ Returns current block offset - amount of blocks added on top of real current blo #### Returns -| Type | Description | -| ------ | ----------------------------------------------------------------------- | +| Type | Description | +| ------ | ------------------------------------------------------------------------------------------- | | string | number representing amount of blocks added on top of real current block (encoded as string) | #### Usage @@ -766,7 +789,7 @@ describe("interactions - sendTransaction", () => { }) ``` -## shallRevert(ix, message) +### shallRevert(ix, message) Ensure interaction throws an error. Can test for specific error messages or catch any error message if `message` is not provided. Returns Promise, which contains result, when resolved. @@ -1009,8 +1032,7 @@ main() ## Transactions -Another common case is necessity to mutate network state - sending tokens from one account to another, minting new -NFT, etc. Framework provides `sendTransaction` method to achieve this. This method has 2 different signatures. +Another common case is interactions that mutate network state - sending tokens from one account to another, minting new NFT, etc. Framework provides `sendTransaction` method to achieve this. This method have 2 different signatures. > ⚠️ **Required:** Your project must follow the [required structure](structure.md) it must be [initialized](init.md) to use the following functions. @@ -1023,14 +1045,13 @@ Provides explicit control over how you pass values. `props` object accepts following fields: -| Name | Type | Optional | Description | -| -------------- | ------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------- | -| `code` | string | ✅ | string representation of Cadence transaction | -| `name` | string | ✅ | name of the file in `transaction` folder to use (sans `.cdc` extension) | -| `args` | [Any] | ✅ | an array of arguments to pass to transaction. Optional if transaction does not expect any arguments. | -| `signers` | [Address] | ✅ | an array of [Address](https://docs.onflow.org/fcl/reference/api/#address) representing transaction autorizers | -| `addressMap` | [AddressMap](#addressmap) | ✅ | name/address map to use as lookup table for addresses in import statements | -| `transformers` | array[[CadenceTransformer](#cadencetransformer)] | ✅ | an array of operators to modify the code, before submitting it to network | +| Name | Type | Optional | Description | +| ------------ | ---------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `code` | string | ✅ | string representation of Cadence transaction | +| `name` | string | ✅ | name of the file in `transaction` folder to use (sans `.cdc` extension) | +| `args` | [Any] | ✅ | an array of arguments to pass to transaction. Optional if transaction does not expect any arguments. | +| `signers` | [[Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfo](./api.md#signerinfoobject)] | ✅ | an array of [Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfo](./api.md#signerinfoobject) objects representing transaction autorizers | +| `addressMap` | [AddressMap](api.md#addressmap) | ✅ | name/address map to use as lookup table for addresses in import statements | > ⚠️ **Required:** Either `code` or `name` field shall be specified. Method will throw an error if both of them are empty. > If `name` field provided, framework will source code from file and override value passed via `code` field. @@ -1040,12 +1061,6 @@ Provides explicit control over how you pass values. > 📣 Pass `addressMap` only in cases, when you would want to override deployed contract. Otherwide > imports can be resolved automatically without explicitly passing them via `addressMap` field -#### Returns - -| Type | Description | -| --------------------------------------------------------------------------- | ------------------ | -| [ResponseObject](https://docs.onflow.org/fcl/reference/api/#responseobject) | Interaction result | - #### Usage ```javascript @@ -1078,7 +1093,7 @@ const main = async () => { const signers = [Alice] const [tx, error] = await sendTransaction({code, args, signers}) - console.log({tx}, {error}) + console.log(tx, error) // Stop emulator instance await emulator.stop() @@ -1092,17 +1107,11 @@ main() This signature provides simplified way to send a transaction, since most of the time you will utilize existing Cadence files. -| Name | Type | Optional | Description | -| --------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------- | -| `name` | string | ✅ | name of the file in `transaction` folder to use (sans `.cdc` extension) | -| `signers` | array | ✅ | an array of [Address](https://docs.onflow.org/fcl/reference/api/#address) representing transaction autorizers | -| `args` | [Any] | ✅ | an array of arguments to pass to transaction. Optional if transaction does not expect any arguments. | - -#### Returns - -| Type | Description | -| --------------------------------------------------------------------------- | ------------------ | -| [ResponseObject](https://docs.onflow.org/fcl/reference/api/#responseobject) | Interaction result | +| Name | Type | Optional | Description | +| --------- | ---------------------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | string | ✅ | name of the file in `transaction` folder to use (sans `.cdc` extension) | +| `args` | [Any] | ✅ | an array of arguments to pass to transaction. Optional if transaction does not expect any arguments. | +| `signers` | [[Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfoObject](./api.md#signerinfoobject)] | ✅ | an array of [Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfoObjects](./api.md#signerinfoobject) representing transaction autorizers | #### Usage @@ -1120,11 +1129,9 @@ const main = async () => { // Define arguments we want to pass const args = ["Hello, Cadence"] - const Alice = await getAccountAddress("Alice") - const signers = [Alice] - const [tx, error] = await sendTransaction("log-message", [Alice], args) - console.log({tx}, {error}) + const [tx, error] = await sendTransaction("log-message", [], args) + console.log(tx, error) } main() @@ -1171,7 +1178,7 @@ const main = async () => { main() ``` -## `getContractCode(name, addressMap)` +### `getContractCode(name, addressMap)` Returns Cadence template from file with `name` in `_basepath_/contracts` folder @@ -1357,3 +1364,94 @@ const replaceAddress = async code => { return modified } ``` + +### KeyObject + +Key objects are used to specify signer keys when [creating accounts](./accounts.md). + +| Key | Required | Value Type | Description | +| -------------------- | -------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `hashAlgorithm` | No | [HashAlgorithm](#hashalgorithm) | Hashing algorithm to use for generating signatures to be signed by this key (default: `HashAlgorithm.SHA3_256`) | +| `privateKey` | Yes | string | Private key to use to generate the signature | +| `signatureAlgorithm` | No | [SignatureAlgorithm](#signaturealgorithm) | Signing algorithm used to sign transactions with this key (default: `SignatureAlgorithm.ECDSA_P256`) | +| `weight` | No | number | Weight of the key - see [Flow Core Concepts](https://docs.onflow.org/concepts/accounts-and-keys/#keys) for more information | + +### PublicKey + +Public keys are stored as `Buffer` objects which have been RLP encoded according to the [Flow spec](https://docs.onflow.org/concepts/accounts-and-keys/). + +In order to generate this object using the Flow JS Testing library, use the [`pubFlowKey` method](#pubflowkeykeyobject) exported by the library. + +```javascript +import {pubFlowKey} from "@onflow/flow-js-testing" + +const pubKey = await pubFlowKey({ + privateKey: ..., + hashAlgorithm: ..., + signatureAlgorithm: ..., + weight: ... +}) +``` + +### SignerInfoObject + +Signer Info objects are used to specify information about which signer and which key from this signer shall be used to [sign a transaction](./send-transactions.md). + +| Key | Required | Value Type | Description | +| -------------------- | -------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `addr` | Yes | [Address](https://docs.onflow.org/fcl/reference/api/#address) | The address of the signer's account | +| `hashAlgorithm` | No | [HashAlgorithm](#hashalgorithm) | Hashing algorithm to use for generating the signature (default: `HashAlgorithm.SHA3_256`) | +| `keyId` | No | number | The index of the desired key to use from the signer's account (default: `0`) | +| `privateKey` | No | string | Private key to use to generate the signature (default: service account private key - this is the default PK for all accounts generated by Flow JS Testing Library, see: [accounts](./accounts.md)) | +| `signatureAlgorithm` | No | [SignatureAlgorithm](#signaturealgorithm) | Signing algorithm used to generate the signature (default: `SignatureAlgorithm.ECDSA_P256`) | + +### HashAlgorithm + +| Identifier | Value | +| ---------- | ----- | +| SHA2_256 | 1 | +| SHA3_256 | 3 | + +Hash algorithms may be provided as either an enum (accessible via the `HashAlgorithm` object exported by Flow JS Testing, i.e. `HashAlgorithm.SHA3_256`) or as a string representation of their enum identifier (i.e. `"SHA3_256"`) + +### SignatureAlgorithm + +| Identifier | Value | +| --------------- | ----- | +| ECDSA_P256 | 2 | +| ECDSA_secp256k1 | 3 | + +Signing algorithms may be provided as either an enum (accessible via the `SignatureAlgorithm` object exported by Flow JS Testing, i.e. `SignatureAlgorithm.ECDSA_P256`) or as a string representation of their enum identifier (i.e. `"ECDSA_P256"`) + +## Utilities + +### `isAddress(address)` + +Returns true if the given string is a validly formatted account [address](https://docs.onflow.org/fcl/reference/api/#address) (both "0x" prefixed and non-prefixed are valid) + +#### Arguments + +| Name | Type | Optional | Description | +| --------- | ------ | -------- | -------------------------------- | +| `address` | string | | string to test against the regex | + +#### Returns + +| Type | Description | +| ------- | -------------------------------------------------------------------------------------------------------------------------- | +| boolean | Returns true if given string is a validly formatted account [address](https://docs.onflow.org/fcl/reference/api/#address). | + +#### Usage + +```javascript +import {isAddress} from "@onflow/flow-js-testing" + +const badAddr = "0xqrtyff" +console.log(isAddress(badAddr)) // false + +const goodAddrWithPrefix = "0xf8d6e0586b0a20c1" +console.log(isAddress(goodAddrWithPrefix)) // true + +const goodAddrSansPrefix = "f8d6e0586b0a20c1" +console.log(isAddress(goodAddrSansPrefix)) // true +``` diff --git a/docs/emulator.md b/docs/emulator.md index 0e691457..e85ef37b 100644 --- a/docs/emulator.md +++ b/docs/emulator.md @@ -13,9 +13,9 @@ Starts emulator on random available port, unless overriden in options. Returns P #### Arguments -| Name | Type | Optional | Description | -| --------- | --------------- | -------- | ------------------------------------------------------ | -| `options` | EmulatorOptions | ✅ | an object containing options for starting the emulator | +| Name | Type | Optional | Description | +| --------- | ----------------------------------- | -------- | ------------------------------------------------------ | +| `options` | [EmulatorOptions](#emulatoroptions) | ✅ | an object containing options for starting the emulator | #### EmulatorOptions diff --git a/docs/send-transactions.md b/docs/send-transactions.md index a8516e96..80ce1a55 100644 --- a/docs/send-transactions.md +++ b/docs/send-transactions.md @@ -17,13 +17,13 @@ Provides explicit control over how you pass values. `props` object accepts following fields: -| Name | Type | Optional | Description | -| ------------ | -------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| `code` | string | ✅ | string representation of Cadence transaction | -| `name` | string | ✅ | name of the file in `transaction` folder to use (sans `.cdc` extension) | -| `args` | [Any] | ✅ | an array of arguments to pass to transaction. Optional if transaction does not expect any arguments. | -| `signers` | [[Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfo](#signerinfo)] | ✅ | an array of [Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfo](#signerinfo) objects representing transaction autorizers | -| `addressMap` | [AddressMap](api.md#addressmap) | ✅ | name/address map to use as lookup table for addresses in import statements | +| Name | Type | Optional | Description | +| ------------ | ---------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `code` | string | ✅ | string representation of Cadence transaction | +| `name` | string | ✅ | name of the file in `transaction` folder to use (sans `.cdc` extension) | +| `args` | [Any] | ✅ | an array of arguments to pass to transaction. Optional if transaction does not expect any arguments. | +| `signers` | [[Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfo](./api.md#signerinfoobject)] | ✅ | an array of [Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfo](./api.md#signerinfoobject) objects representing transaction autorizers | +| `addressMap` | [AddressMap](api.md#addressmap) | ✅ | name/address map to use as lookup table for addresses in import statements | > ⚠️ **Required:** Either `code` or `name` field shall be specified. Method will throw an error if both of them are empty. > If `name` field provided, framework will source code from file and override value passed via `code` field. @@ -79,11 +79,11 @@ main() This signature provides simplified way to send a transaction, since most of the time you will utilize existing Cadence files. -| Name | Type | Optional | Description | -| --------- | --------- | -------- | ---------------------------------------------------------------------------------------------------- | -| `name` | string | ✅ | name of the file in `transaction` folder to use (sans `.cdc` extension) | -| `args` | [Any] | ✅ | an array of arguments to pass to transaction. Optional if transaction does not expect any arguments. | -| `signers` | [Address] | ✅ | an array of [Address](#ddress) representing transaction autorizers | +| Name | Type | Optional | Description | +| --------- | ---------------------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | string | ✅ | name of the file in `transaction` folder to use (sans `.cdc` extension) | +| `args` | [Any] | ✅ | an array of arguments to pass to transaction. Optional if transaction does not expect any arguments. | +| `signers` | [[Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfoObject](./api.md#signerinfoobject)] | ✅ | an array of [Address](https://docs.onflow.org/fcl/reference/api/#address) or [SignerInfoObjects](./api.md#signerinfoobject) representing transaction autorizers | #### Usage @@ -108,14 +108,3 @@ const main = async () => { main() ``` - -## Objects, structs, enums - -### SignerInfo - -| Key | Required | Value Type | Description | -| --------------- | -------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `addr` | Yes | [Address](https://docs.onflow.org/fcl/reference/api/#address) | The address of the signer's account | -| `hashAlgorithm` | No | string | Hashing algorithm to use for the signature (either `"p256"` or `"secp256k1"`, default: `"p256"`) | -| `privateKey` | No | string | Private key to use to generate the signature (default: service account private key - this is the default PK for all accounts generated by Flow JS Testing Library, see: [accounts](./accounts.md)) | -| `keyId` | No | number | The index of the desired key to use from the signer's account (default: `0`) | diff --git a/examples/01-get-account-address.test.js b/examples/01-get-account-address.test.js index 122f34e8..d87cbcd4 100644 --- a/examples/01-get-account-address.test.js +++ b/examples/01-get-account-address.test.js @@ -1,5 +1,5 @@ import path from "path" -import {init, emulator, getAccountAddress} from "../src" +import {init, emulator, getAccountAddress, isAddress} from "../src" beforeEach(async () => { const basePath = path.resolve(__dirname, "./cadence") @@ -12,7 +12,7 @@ test("get account address", async () => { const Alice = await getAccountAddress("Alice") // Expect Alice to be address of Alice's account - expect(Alice).toMatch(/^0x[0-9a-f]{16}$/) + expect(isAddress(Alice)).toBe(true) }) afterEach(async () => { diff --git a/examples/04-get-contract-address.test.js b/examples/04-get-contract-address.test.js index 85f919e7..6707a21b 100644 --- a/examples/04-get-contract-address.test.js +++ b/examples/04-get-contract-address.test.js @@ -1,5 +1,11 @@ import path from "path" -import {init, emulator, deployContractByName, getContractAddress} from "../src" +import { + init, + emulator, + deployContractByName, + getContractAddress, + isAddress, +} from "../src" beforeEach(async () => { const basePath = path.resolve(__dirname, "./cadence") @@ -16,7 +22,7 @@ test("get contract address", async () => { const contractAddress = await getContractAddress("Hello") // Expect contractAddress to be address - expect(contractAddress).toMatch(/^0x[0-9a-f]{16}$/) + expect(isAddress(contractAddress)).toBe(true) }) afterEach(async () => { diff --git a/examples/09-send-transaction.test.js b/examples/09-send-transaction.test.js index 209b0826..1c85754c 100644 --- a/examples/09-send-transaction.test.js +++ b/examples/09-send-transaction.test.js @@ -1,4 +1,3 @@ -import {config} from "@onflow/fcl" import path from "path" import {init, emulator, getAccountAddress, sendTransaction} from "../src" @@ -25,16 +24,7 @@ test("send transaction", async () => { } } ` - - const signers = [ - { - addr: Alice, - keyId: 0, - privateKey: await config().get("PRIVATE_KEY"), - hashAlgorithm: "p256", - }, - Bob, - ] + const signers = [Alice, Bob] const args = ["Hello from Cadence"] // There are several ways to call "sendTransaction" diff --git a/package-lock.json b/package-lock.json index bcb95a6e..76b660d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,10 @@ "elliptic": "^6.5.4", "esm": "^3.2.25", "jest-environment-uint8array": "^1.0.0", + "js-sha256": "^0.9.0", + "js-sha3": "^0.8.0", "rimraf": "^3.0.2", "rlp": "^2.2.6", - "sha3": "^2.1.4", "yargs": "^17.0.1" }, "bin": { @@ -41,6 +42,12 @@ "prettier": "^2.2.1" } }, + "git+github.com/onflow/flow-emulator.git": { + "extraneous": true + }, + "github.com/onflow/flow-emulator.git": { + "extraneous": true + }, "node_modules/@babel/code-frame": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", @@ -9458,6 +9465,16 @@ "node": ">=10" } }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -22159,6 +22176,16 @@ } } }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 6047539f..61433049 100644 --- a/package.json +++ b/package.json @@ -56,9 +56,10 @@ "elliptic": "^6.5.4", "esm": "^3.2.25", "jest-environment-uint8array": "^1.0.0", + "js-sha256": "^0.9.0", + "js-sha3": "^0.8.0", "rimraf": "^3.0.2", "rlp": "^2.2.6", - "sha3": "^2.1.4", "yargs": "^17.0.1" }, "devDependencies": { diff --git a/src/account.js b/src/account.js index 5619b87e..5ba94f82 100644 --- a/src/account.js +++ b/src/account.js @@ -16,11 +16,48 @@ * limitations under the License. */ -import {pubFlowKey} from "./crypto" import {executeScript, sendTransaction} from "./interaction" import {getManagerAddress} from "./manager" import registry from "./generated" +import {config} from "@onflow/fcl" +import {pubFlowKey} from "./crypto" +import {isObject} from "./utils" + +export async function createAccount({name, keys}) { + if (!keys) { + keys = [ + { + privateKey: await config().get("PRIVATE_KEY"), + }, + ] + } + + // If public key is encoded already, don't change + // If provided as KeyObject (private key) generate public key + keys = await Promise.all( + keys.map(key => (isObject(key) ? pubFlowKey(key) : key)) + ) + + const managerAddress = await getManagerAddress() + const addressMap = { + FlowManager: managerAddress, + } + + const code = await registry.transactions.createAccountTemplate(addressMap) + const args = [name, keys, managerAddress] + + const [result, error] = await sendTransaction({ + code, + args, + }) + if (error) throw error + const {events} = result + const event = events.find(event => event.type.includes("AccountAdded")) + const address = event?.data?.address + + return address +} /** * Returns address of account specified by name. If account with that name doesn't exist it will be created @@ -53,17 +90,7 @@ export const getAccountAddress = async accountName => { accountAddress = result if (accountAddress === null) { - const code = await registry.transactions.createAccountTemplate(addressMap) - const publicKey = await pubFlowKey() - const args = [name, publicKey, managerAddress] - - const [result] = await sendTransaction({ - code, - args, - }) - const {events} = result - const event = events.find(event => event.type.includes("AccountAdded")) - accountAddress = event.data.address + accountAddress = await createAccount({name}) } return accountAddress } diff --git a/src/crypto.js b/src/crypto.js index 9e932033..f72ed4c9 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -16,23 +16,84 @@ * limitations under the License. */ -import {ec as EC} from "elliptic" -import {SHA3} from "sha3" import * as fcl from "@onflow/fcl" import * as rlp from "rlp" +import {ec as EC} from "elliptic" import {config} from "@onflow/config" -import {isObject} from "./utils" +import {isObject, isString} from "./utils" + +import {sha3_256} from "js-sha3" +import {sha256 as sha2_256} from "js-sha256" + +export const SignatureAlgorithm = { + ECDSA_P256: 2, + ECDSA_secp256k1: 3, +} + +export const HashAlgorithm = { + SHA2_256: 1, + SHA3_256: 3, +} + +export const HashAlgoFnMap = { + SHA2_256: sha2_256, + SHA3_256: sha3_256, +} -const hashMsgHex = msgHex => { - const sha = new SHA3(256) - sha.update(Buffer.from(msgHex, "hex")) - return sha.digest() +export const SignAlgoECMap = { + ECDSA_P256: "p256", + ECDSA_secp256k1: "secp256k1", +} + +export const resolveHashAlgoKey = hashAlgorithm => { + const hashAlgorithmKey = Object.keys(HashAlgorithm).find( + x => + HashAlgorithm[x] === hashAlgorithm || + (isString(hashAlgorithm) && + x.toLowerCase() === hashAlgorithm.toLowerCase()) + ) + if (!hashAlgorithmKey) + throw new Error( + `Provided hash algorithm "${hashAlgorithm}" is not currently supported` + ) + return hashAlgorithmKey } -export const signWithKey = (privateKey, msgHex, hashAlgorithm) => { - const ec = new EC(hashAlgorithm) +export const resolveSignAlgoKey = signatureAlgorithm => { + const signatureAlgorithmKey = Object.keys(SignatureAlgorithm).find( + x => + SignatureAlgorithm[x] === signatureAlgorithm || + (isString(signatureAlgorithm) && + x.toLowerCase() === signatureAlgorithm.toLowerCase()) + ) + if (!signatureAlgorithmKey) + throw new Error( + `Provided signature algorithm "${signatureAlgorithm}" is not currently supported` + ) + return signatureAlgorithmKey +} + +const hashMsgHex = (msgHex, hashAlgorithm = HashAlgorithm.SHA3_256) => { + const hashAlgorithmKey = resolveHashAlgoKey(hashAlgorithm) + const hashFn = HashAlgoFnMap[hashAlgorithmKey] + + const hash = hashFn.create() + hash.update(Buffer.from(msgHex, "hex")) + return Buffer.from(hash.arrayBuffer()) +} + +export const signWithKey = ( + privateKey, + msgHex, + hashAlgorithm = HashAlgorithm.SHA3_256, + signatureAlgorithm = SignatureAlgorithm.ECDSA_P256 +) => { + const signatureAlgorithmKey = resolveSignAlgoKey(signatureAlgorithm) + const curve = SignAlgoECMap[signatureAlgorithmKey] + + const ec = new EC(curve) const key = ec.keyFromPrivate(Buffer.from(privateKey, "hex")) - const sig = key.sign(hashMsgHex(msgHex)) + const sig = key.sign(hashMsgHex(msgHex, hashAlgorithm)) const n = 32 // half of signature length? const r = sig.r.toArrayLike(Buffer, "be", n) const s = sig.s.toArrayLike(Buffer, "be", n) @@ -47,7 +108,8 @@ export const authorization = let addr = serviceAddress, keyId = 0, privateKey = await config().get("PRIVATE_KEY"), - hashAlgorithm = "p256" + hashAlgorithm = HashAlgorithm.SHA3_256, + signatureAlgorithm = SignatureAlgorithm.ECDSA_P256 if (isObject(signer)) { ;({ @@ -55,6 +117,7 @@ export const authorization = keyId = keyId, privateKey = privateKey, hashAlgorithm = hashAlgorithm, + signatureAlgorithm = signatureAlgorithm, } = signer) } else { addr = signer || addr @@ -63,7 +126,12 @@ export const authorization = const signingFunction = async data => ({ keyId, addr: addr, - signature: signWithKey(privateKey, data.message, hashAlgorithm), + signature: signWithKey( + privateKey, + data.message, + hashAlgorithm, + signatureAlgorithm + ), }) return { @@ -75,18 +143,29 @@ export const authorization = } } -export const pubFlowKey = async () => { - const ec = new EC("p256") - const keys = ec.keyFromPrivate( - Buffer.from(await config().get("PRIVATE_KEY"), "hex") - ) +export const pubFlowKey = async ({ + privateKey, + hashAlgorithm = HashAlgorithm.SHA3_256, + signatureAlgorithm = SignatureAlgorithm.ECDSA_P256, + weight = 1000, // give key full weight +}) => { + // Converty hex string private key to buffer if not buffer already + if (!Buffer.isBuffer(privateKey)) privateKey = Buffer.from(privateKey, "hex") + + const hashAlgoName = resolveHashAlgoKey(hashAlgorithm) + const sigAlgoName = resolveSignAlgoKey(signatureAlgorithm) + + const curve = SignAlgoECMap[sigAlgoName] + + const ec = new EC(curve) + const keys = ec.keyFromPrivate(privateKey) const publicKey = keys.getPublic("hex").replace(/^04/, "") return rlp .encode([ Buffer.from(publicKey, "hex"), // publicKey hex to binary - 2, // P256 per https://github.com/onflow/flow/blob/master/docs/accounts-and-keys.md#supported-signature--hash-algorithms - 3, // SHA3-256 per https://github.com/onflow/flow/blob/master/docs/accounts-and-keys.md#supported-signature--hash-algorithms - 1000, // give key full weight + SignatureAlgorithm[sigAlgoName], + HashAlgorithm[hashAlgoName], + weight, ]) .toString("hex") } diff --git a/src/generated/transactions/createAccount.js b/src/generated/transactions/createAccount.js index 7697c64c..afeb2efa 100644 --- a/src/generated/transactions/createAccount.js +++ b/src/generated/transactions/createAccount.js @@ -9,23 +9,27 @@ import { } from '@onflow/flow-cadut' export const CODE = ` -import FlowManager from 0x01 - -transaction (_ name: String, pubKey: String, manager: Address) { - prepare( admin: AuthAccount) { - let newAccount = AuthAccount(payer:admin) - newAccount.addPublicKey(pubKey.decodeHex()) - - let linkPath = FlowManager.accountManagerPath - let accountManager = getAccount(manager) - .getCapability(linkPath)! - .borrow<&FlowManager.Mapper>()! - - // Create a record in account database - let address = newAccount.address - accountManager.setAddress(name, address: address) - } -} +import FlowManager from 0x01 + +transaction (_ name: String?, pubKey: [String], manager: Address) { + prepare( admin: AuthAccount) { + let newAccount = AuthAccount(payer:admin) + for key in pubKey { + newAccount.addPublicKey(key.decodeHex()) + } + + if name != nil { + let linkPath = FlowManager.accountManagerPath + let accountManager = getAccount(manager) + .getCapability(linkPath)! + .borrow<&FlowManager.Mapper>()! + + // Create a record in account database + let address = newAccount.address + accountManager.setAddress(name!, address: address) + } + } +} `; diff --git a/src/index.js b/src/index.js index ff9c3fa5..d623cc19 100644 --- a/src/index.js +++ b/src/index.js @@ -27,7 +27,7 @@ export { export {sendTransaction, executeScript} from "./interaction" export {getFlowBalance, mintFlow} from "./flow-token" export {deployContract, deployContractByName} from "./deploy-code" -export {getAccountAddress} from "./account" +export {createAccount, getAccountAddress} from "./account" export { getServiceAddress, getBlockOffset, @@ -45,5 +45,7 @@ export { shallThrow, } from "./jest-asserts" export {builtInMethods} from "./transformers" +export {HashAlgorithm, SignatureAlgorithm, pubFlowKey} from "./crypto" +export {isAddress} from "./utils" export {default as emulator} from "./emulator" diff --git a/src/utils.js b/src/utils.js index 16a7e7f6..ab8fb8ef 100644 --- a/src/utils.js +++ b/src/utils.js @@ -19,6 +19,8 @@ import {createServer} from "net" export const isObject = arg => typeof arg === "object" && arg !== null +export const isString = obj => typeof obj === "string" || obj instanceof String +export const isAddress = address => /^0x[0-9a-f]{0,16}$/.test(address) export function getAvailablePorts(count = 1) { if (count === 0) return Promise.resolve([])