Skip to content
This repository has been archived by the owner on Nov 5, 2024. It is now read-only.

Commit

Permalink
OSS Support Grant: Milestone 2 - Add methods to inspect account stora…
Browse files Browse the repository at this point in the history
…ge (#215)

* Add default addresses for more contracts

* Add methods to query account storage

* Add tests to cover newly added methods

* Add license header

* Add additional messages for storage inspection

* Add tests for utility methods

* Implement storage inspection methods. Add tests.

* Add rule to allow simple expects with helpers

* Add Jest helpers for storage inspection

* Update documentation

* Add changeset

* Update broken link for Cadence Transformer

* Update logger regexp
  • Loading branch information
MaxStalker authored Apr 28, 2023
1 parent 4e1d666 commit 7c302b8
Show file tree
Hide file tree
Showing 11 changed files with 836 additions and 14 deletions.
7 changes: 7 additions & 0 deletions .changeset/modern-guests-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@onflow/flow-js-testing": minor
---

- Storage inspection API implemented as set of utility methods
- Additional Jest helpers implemented
- Related documentation added
4 changes: 3 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module.exports = {
ecmaVersion: 12,
sourceType: "module",
},
rules: {},
rules: {
"jest/expect-expect": "off",
},
plugins: ["jest"],
}
218 changes: 211 additions & 7 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,12 @@ If `keyObject` is not provided, Flow JS Testing will default to the [universal p
#### Usage

```javascript
import {pubFlowKey}
import {pubFlowKey} from "@onflow/flow-js-testing"

const key = {
privateKey: "a1b2c3" // private key as hex string
hashAlgorithm: HashAlgorithm.SHA3_256
signatureAlgorithm: SignatureAlgorithm.ECDSA_P256
privateKey: "a1b2c3", // private key as hex string
hashAlgorithm: HashAlgorithm.SHA3_256,
signatureAlgorithm: SignatureAlgorithm.ECDSA_P256,
weight: 1000
}

Expand Down Expand Up @@ -935,7 +935,7 @@ describe("interactions - sendTransaction", () => {
)

// Catch only specific panic message
let [txResult, error] = await shallRevert(
let [txResult2, error2] = await shallRevert(
sendTransaction({
code,
signers,
Expand Down Expand Up @@ -1135,13 +1135,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](./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 |
| `transformers` | [[CadenceTransformer](./#cadencetransformer)] || an array of operators to modify the code, before submitting it to network |
| `transformers` | [[CadenceTransformer](./api.md#cadencetransformer)] || an array of operators to modify the code, before submitting it to network |

> ⚠️ **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.
Expand Down Expand Up @@ -1415,6 +1415,210 @@ const main = async () => {
main()
```

## Storage Inspection

### getPaths

Retrieves information about the public, private, and storage paths for a given account.

#### Arguments

| Name | Type | Description |
| ------------------- | --------- | --------------------------------------------------------------------- |
| `address` | `string` | The address or name of the account to retrieve the paths from. |
| `useSet` (optional) | `boolean` | Whether to return the paths as a Set or an array. Defaults to `true`. |

#### Returns

An object containing the following properties:

| Name | Type | Description |
| -------------- | -------------------------------- | ----------------------------------------------------------------- |
| `publicPaths` | `Array<string>` or `Set<string>` | An array or Set of the public paths for the account, as strings. |
| `privatePaths` | `Array<string>` or `Set<string>` | An array or Set of the private paths for the account, as strings. |
| `storagePaths` | `Array<string>` or `Set<string>` | An array or Set of the storage paths for the account, as strings. |

> The `useSet` parameter determines whether the paths are returned as an array or Set. If `useSet` is `true`, the paths will be returned as a Set; otherwise, they will be returned as an array.
#### Usage

```js
import path from "path"
import {init, emulator} from "@onflow/flow-js-testing"
import {getAccountAddress, getPaths} from "@onflow/flow-js-testing"

const main = async () => {
const basePath = path.resolve(__dirname, "../cadence")

await init(basePath)
await emulator.start()

// Get storage stats
const Alice = await getAccountAddress("Alice")
const paths = await getPaths(Alice)
const {publicPaths, privatePaths, storagePaths} = paths

// Output result to console
console.log({Alice, paths})

await emulator.stop()
}

main()
```

### getPathsWithType

Retrieves public, private, and storage paths for a given account with extra information available on them

#### Arguments

| Name | Type | Description |
| --------- | -------- | -------------------------------------------------------------- |
| `address` | `string` | The address or name of the account to retrieve the paths from. |

#### Returns

An object containing the following properties:

| Name | Type | Description |
| -------------- | -------- | ------------------------------------------------------------------------------------------ |
| `publicPaths` | `Object` | An object containing the public paths for the account, as keys and their types as values. |
| `privatePaths` | `Object` | An object containing the private paths for the account, as keys and their types as values. |
| `storagePaths` | `Object` | An object containing the storage paths for the account, as keys and their types as values. |

> The types of the paths are not strictly defined and may vary depending on the actual types used in the account.
#### Usage

```js
import path from "path"
import {init, emulator} from "@onflow/flow-js-testing"
import {getPathsWithType} from "@onflow/flow-js-testing"

const main = async () => {
const basePath = path.resolve(__dirname, "../cadence")

await init(basePath)
await emulator.start()

const {publicPaths} = await getPathsWithType("Alice")
const refTokenBalance = publicPaths.flowTokenBalance

if (
refTokenBalance.restrictionsList.has(
"A.ee82856bf20e2aa6.FungibleToken.Balance"
)
) {
console.log("Found specific restriction")
}

if (refTokenBalance.haveRestrictions("FungibleToken.Balance")) {
console.log("Found matching restriction")
}

await emulator.stop()
}

main()
```

### getStorageValue

#### Arguments

| Name | Type | Description |
| --------- | -------- | ---------------------------------------------------------------------- |
| `account` | `string` | The address or name of the account to retrieve the storage value from. |
| `path` | `string` | The path of the storage value to retrieve. |

#### Returns

| Type | Description |
| -------------- | --------------------------------------------------------------------------- |
| `Promise<any>` | The value of the storage at the given path, or `null` if no value is found. |

#### Usage

```js
import path from "path"
import {init, emulator} from "@onflow/flow-js-testing"
import {sendTransaction, getStorageValue} from "@onflow/flow-js-testing"

const main = async () => {
const basePath = path.resolve(__dirname, "../cadence")

await init(basePath)
await emulator.start()

// Inplant some value into account
await sendTransaction({
code: `
transaction{
prepare(signer: AuthAccount){
signer.save(42, to: /storage/answer)
}
}
`,
signers: [Alice],
})
const answer = await getStorageValue("Alice", "answer")
console.log({answer})

await emulator.stop()
}

main()
```

### getStorageStats

Retrieves the storage statistics (used and capacity) for a given account.

#### Arguments

| Name | Type | Description |
| -------------------- | --------- | ------------------------------------------------------------------------------------------------ |
| `address` | `string` | The address or name of the account to check for storage statistics. |
| `convert` (optional) | `boolean` | Whether to convert the `used` and `capacity` values from strings to numbers. Defaults to `true`. |

#### Returns

A Promise that resolves to an object containing the following properties:

| Name | Type | Description |
| ---------- | -------------------- | ---------------------------------------------------- |
| `used` | `number` or `string` | The amount of storage used by the account, in bytes. |
| `capacity` | `number` or `string` | The total storage capacity of the account, in bytes. |

> If `convert` is `true`, the `used` and `capacity` values will be converted from strings to numbers before being returned.
#### Usage

```js
import path from "path"
import {init, emulator} from "@onflow/flow-js-testing"
import {getAccountAddress, getStorageStats} from "@onflow/flow-js-testing"

const main = async () => {
const basePath = path.resolve(__dirname, "../cadence")

await init(basePath)
await emulator.start()

// Get storage stats
const Alice = await getAccountAddress("Alice")
const {capacity, used} = await getStorageStats(Alice)

// Output result to console
console.log({Alice, capacity, used})

await emulator.stop()
}

main()
```

## Types

### `AddressMap`
Expand Down
72 changes: 72 additions & 0 deletions docs/jest-helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,75 @@ describe("interactions - sendTransaction", () => {
})
})
```

## shallHavePath(account, path)

Asserts that the given account has the given path enabled.

#### Arguments

| Name | Type | Description |
| --------- | -------- | --------------------------------------------------------- |
| `account` | `string` | The address or name of the account to check for the path. |
| `path` | `string` | The path to check for. |

#### Returns

| Type | Description |
| --------------- | --------------------------------------------------------------------------------------------- |
| `Promise<void>` | A Promise that resolves when the assertion is complete, or rejects with an error if it fails. |

#### Usage

```javascript
import path from "path"
import {init, emulator, shallPass, executeScript} from "js-testing-framework"

// We need to set timeout for a higher number, cause some interactions might need more time
jest.setTimeout(10000)

describe("interactions - sendTransaction", () => {
// Instantiate emulator and path to Cadence files
beforeEach(async () => {
const basePath = path.resolve(__dirname, "./cadence")
await init(basePath)
return emulator.start()
})

// Stop emulator, so it could be restarted
afterEach(async () => {
return emulator.stop()
})

describe("check path with Jest helper", () => {
test("pass account address", async () => {
const Alice = await getAccountAddress("Alice")
await shallHavePath(Alice, "/storage/flowTokenVault")
})

test("pass account name", async () => {
await shallHavePath("Alice", "/storage/flowTokenVault")
})
})
})
```

## shallHaveStorageValue(account, params)

Asserts that the given account has the expected storage value at the given path.

#### Arguments

| Name | Type | Description |
| ----------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `account` | `string` | The address or name of the account to check for the storage value. |
| `params` | `{pathName: string, key?: string, expect: any}` | An object containing the path name, optional key, and expected value of the storage at the given path. |
| `params.pathName` | `string` | The path of the storage value to retrieve. |
| `params.key` | `string` (optional) | The key of the value to retrieve from the storage at the given path, if applicable. |
| `expect` | `any` | The expected value of the storage at the given path and key (if applicable). |

#### Returns

| Type | Description |
| --------------- | --------------------------------------------------------------------------------------------- |
| `Promise<void>` | A Promise that resolves when the assertion is complete, or rejects with an error if it fails. |
12 changes: 6 additions & 6 deletions src/emulator/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ export const LOGGER_LEVELS = {
TRACE: -1,
}

const LOG_REGEXP =
// eslint-disable-next-line no-control-regex
/\x1B\[1;34mLOG\x1B\[0m \x1B\[2m\[[a-z0-9]{6}]\x1B\[0m "(.*)"/
// eslint-disable-next-line no-control-regex
const LOG_REGEXP = /LOG:.*?\s+(.*)/

export class Logger extends EventEmitter {
constructor(options) {
Expand Down Expand Up @@ -66,13 +65,14 @@ export class Logger extends EventEmitter {
// Handle log special case
const levelMatch =
level === LOGGER_LEVELS.INFO || level === LOGGER_LEVELS.DEBUG
if (levelMatch && LOG_REGEXP.test(msg)) {
let logMessage = msg.match(LOG_REGEXP).at(1)

const logMatch = LOG_REGEXP.test(msg)
if (levelMatch && logMatch) {
let logMessage = msg.match(LOG_REGEXP).at(1)
// if message is string, remove from surrounding and unescape
if (/^"(.*)"/.test(logMessage)) {
logMessage = logMessage
.substring(1, logMessage.length - 2)
.substring(1, logMessage.length - 1)
.replace(/\\"/g, '"')
}

Expand Down
6 changes: 6 additions & 0 deletions src/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export const defaultsByName = {
FungibleToken: "0xee82856bf20e2aa6",
FlowFees: "0xe5a8b7f23e8b548f",
FlowStorageFees: "0xf8d6e0586b0a20c7",

FUSD: "0xf8d6e0586b0a20c7",
NonFungibleToken: "0xf8d6e0586b0a20c7",
MetadataViews: "0xf8d6e0586b0a20c7",
NFTStorefront: "0xf8d6e0586b0a20c7",
NFTStorefrontV2: "0xf8d6e0586b0a20c7",
}

/**
Expand Down
Loading

0 comments on commit 7c302b8

Please sign in to comment.