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

OSS Support Grant: Milestone 2 - Add methods to inspect account storage #214

Closed
wants to merge 11 commits into from
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"],
}
206 changes: 205 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
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 @@ -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. |
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
46 changes: 46 additions & 0 deletions src/jest-asserts.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
* limitations under the License.
*/

import {getPaths, getStorageValue} from "./storage"
import {getValueByKey, parsePath} from "./utils"

const {expect} = global

/**
Expand Down Expand Up @@ -122,3 +125,46 @@ export const shallThrow = async ix => {

return response
}

/**
* Asserts that the given account has the given path enabled.
*
* @async
* @param {string} account - The address or name of the account to check for the path.
* @param {string} path - The path to check for.
* @returns {Promise<void>} A Promise that resolves when the assertion is complete, or rejects with an error if the assertion fails.
*/
export const shallHavePath = async (account, path) => {
let parsedPath
expect(() => {
parsedPath = parsePath(path)
}).not.toThrowError()

const {domain, slot} = parsedPath
const paths = await getPaths(account)
const key = `${domain}Paths`
const hasPathEnabled = paths[key].has(slot)

await expect(hasPathEnabled).toBe(true)
}

/**
* Asserts that the given account has the expected storage value at the given path.
*
* @async
* @param {string} account - The address or name of the account to check for the storage value.
* @param {{pathName: string, key?: string, expect: any}} params - An object containing the path name, optional key, and expected value.
* @returns {Promise<void>} A Promise that resolves when the assertion is complete, or rejects with an error if the assertion fails.
*/
export const shallHaveStorageValue = async (account, params) => {
const {pathName, key} = params

const storageValue = await getStorageValue(account, pathName)

if (key) {
const actualValue = getValueByKey(key, storageValue)
expect(actualValue).toBe(params.expect)
} else {
expect(storageValue).toBe(params.expect)
}
}
Loading