Skip to content

Commit

Permalink
Merge pull request #736 from massalabs/getdatastoreentry_null_when_no…
Browse files Browse the repository at this point in the history
…_value

if non existing datastore entry return null
  • Loading branch information
fleandrei authored Feb 10, 2025
2 parents 3e804d9 + 7ea935d commit 6bf0457
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 13 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"test:browser": "jest --config=jest.browser.config.js",
"test:cov": "jest --detectOpenHandles --coverage --forceExit",
"test:cov:watch": "jest --detectOpenHandles --coverage --watch",
"test:integration": "jest --forceExit --runInBand ./test/integration",
"test:integration": "jest --detectOpenHandles --forceExit --runInBand ./test/integration",
"test:all": "npm run test && npm run test:integration",
"test:watch:all": "jest --watchAll",
"check-types": "tsc --noEmit",
Expand Down
6 changes: 3 additions & 3 deletions src/client/publicAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export class PublicAPI {
async getDatastoreEntries(
inputs: DatastoreEntry[],
final = true
): Promise<Uint8Array[]> {
): Promise<(Uint8Array | null)[]> {
const entriesQuery = inputs.map((entry) => {
const byteKey: Uint8Array =
typeof entry.key === 'string' ? strToBytes(entry.key) : entry.key
Expand All @@ -158,15 +158,15 @@ export class PublicAPI {
const res = await this.connector.get_datastore_entries(entriesQuery)
return res.map((r: t.DatastoreEntryOutput) => {
const val = final ? r.final_value : r.candidate_value
return Uint8Array.from(val ?? [])
return val ? Uint8Array.from(val) : null
})
}

async getDatastoreEntry(
key: string | Uint8Array,
address: string,
final = true
): Promise<Uint8Array> {
): Promise<Uint8Array | null> {
return this.getDatastoreEntries([{ key, address }], final).then((r) => r[0])
}

Expand Down
20 changes: 17 additions & 3 deletions src/contracts-wrappers/mns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Operation } from '../operation'
import { Provider, PublicProvider } from '../provider'
import { CallSCOptions, ReadSCOptions, SmartContract } from '../smartContracts'
import { checkNetwork } from './tokens'
import { ErrorDataEntryNotFound } from '../errors/dataEntryNotFound'

export const MNS_CONTRACTS = {
mainnet: 'AS1q5hUfxLXNXLKsYQVXZLK7MPUZcWaNZZsK7e9QzqhGdAgLpUGT',
Expand Down Expand Up @@ -104,8 +105,12 @@ export class MNS extends SmartContract {
...strToBytes(name),
])
const data = await this.provider.readStorage(this.address, [key], true)
if (!data.length) {
throw new Error(`Domain ${name} not found`)
if (data[0] == null) {
throw new ErrorDataEntryNotFound({
key: key,
address: this.address,
details: `mns Domain ${name} not found`,
})
}
return U256.fromBytes(data[0])
}
Expand Down Expand Up @@ -151,7 +156,16 @@ export class MNS extends SmartContract {
domainKeys,
final
)
return domainsBytes.map((d) => bytesToStr(d))
return domainsBytes.map((d, i) => {
if (!d) {
throw new ErrorDataEntryNotFound({
key: ownedKeys[i],
address: this.address,
details: `Domain with tokenId ${U256.fromBytes(ownedKeys[i].slice(filter.length))} not found`,
})
}
return bytesToStr(d as Uint8Array)
})
}

async getTargets(domains: string[], final = false): Promise<string[]> {
Expand Down
21 changes: 20 additions & 1 deletion src/contracts-wrappers/token.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Args, bytesToStr, U256, U8 } from '../basicElements'
import { Operation } from '../operation'
import { CallSCOptions, ReadSCOptions, SmartContract } from '../smartContracts'
import { ErrorDataEntryNotFound } from '../errors/dataEntryNotFound'

/**
* @class MRC20
Expand Down Expand Up @@ -44,6 +45,9 @@ export class MRC20 extends SmartContract {
return this._name
}
const res = await this.provider.readStorage(this.address, ['NAME'], true)
if (!res[0]) {
throw new ErrorDataEntryNotFound({ key: 'NAME', address: this.address })
}
return (this._name = bytesToStr(res[0]))
}

Expand All @@ -52,6 +56,9 @@ export class MRC20 extends SmartContract {
return this._symbol
}
const res = await this.provider.readStorage(this.address, ['SYMBOL'], true)
if (!res[0]) {
throw new ErrorDataEntryNotFound({ key: 'SYMBOL', address: this.address })
}
return (this._symbol = bytesToStr(res[0]))
}

Expand All @@ -64,6 +71,12 @@ export class MRC20 extends SmartContract {
['DECIMALS'],
true
)
if (!res[0]) {
throw new ErrorDataEntryNotFound({
key: 'DECIMALS',
address: this.address,
})
}
return (this._decimals = Number(U8.fromBytes(res[0])))
}

Expand All @@ -73,6 +86,12 @@ export class MRC20 extends SmartContract {
['TOTAL_SUPPLY'],
final
)
if (!res[0]) {
throw new ErrorDataEntryNotFound({
key: 'TOTAL_SUPPLY',
address: this.address,
})
}
return U256.fromBytes(res[0])
}

Expand Down Expand Up @@ -102,7 +121,7 @@ export class MRC20 extends SmartContract {

return res.map((v, i) => ({
address: addresses[i],
balance: v.length ? U256.fromBytes(v) : 0n,
balance: v ? U256.fromBytes(v) : 0n,
}))
}

Expand Down
34 changes: 34 additions & 0 deletions src/errors/dataEntryNotFound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ErrorBase } from './base'
import { ErrorCodes } from './utils/codes'

type ErrorDataEntryNotFoundParameters = {
key: Uint8Array | string
address: string
details?: string
}

/**
* Error class for handling the situation when a data entry has not been found in a smart contract datastore.
*/
export class ErrorDataEntryNotFound extends ErrorBase {
/**
* Override the name to clearly identify this as a ErrorDataEntryNotFound.
*/
override name = 'ErrorDataEntryNotFound'

/**
* Constructs a ErrorDataEntryNotFound with a message indicating the missing data entry.
* @param key - The key of the data entry that was not found.
* @param address - The address of the smart contract datastore where the entry was expected.
* @param details - Optional details to provide more context about the error.
*/
constructor({ key, address, details }: ErrorDataEntryNotFoundParameters) {
super(
`The data entry with key ${key} was not found in the datastore of the contract at address ${address}.`,
{
code: ErrorCodes.DataEntryNotFound,
details,
}
)
}
}
1 change: 1 addition & 0 deletions src/errors/utils/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export enum ErrorCodes {
MaxGasLimit,
InsufficientBalance,
MinimalFee,
DataEntryNotFound,
}
2 changes: 1 addition & 1 deletion src/provider/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type PublicProvider = {
address: string,
keys: Uint8Array[] | string[],
final?: boolean
): Promise<Uint8Array[]>
): Promise<(Uint8Array | null)[]>
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/provider/jsonRpcProvider/jsonRpcPublicProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class JsonRpcPublicProvider implements PublicProvider {
address: string,
keys: Uint8Array[] | string[],
final = true
): Promise<Uint8Array[]> {
): Promise<(Uint8Array | null)[]> {
const entries: DatastoreEntry[] = keys.map((key) => ({ address, key }))
return this.client.getDatastoreEntries(entries, final)
}
Expand Down
36 changes: 36 additions & 0 deletions test/integration/provider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { CHAIN_ID, NetworkName } from '../../src'
import { provider } from './setup'
import { strToBytes, U256 } from '../../src/basicElements/serializers'

const USDC = 'AS12k8viVmqPtRuXzCm6rKXjLgpQWqbuMjc37YHhB452KSUUb9FgL'

describe('Provider tests', () => {
test('networkInfos', async () => {
Expand Down Expand Up @@ -33,3 +36,36 @@ describe('Provider tests', () => {
expect(status.minimalFees).toBeDefined()
})
})

describe('Provider readStorage test', () => {
test('readStorage', async () => {
const dataentries = await provider.readStorage(USDC, [
'NAME',
'SYMBOL',
'TOTAL_SUPPLY',
])
expect(dataentries).toHaveLength(3)
expect(dataentries[0]).toEqual(strToBytes('Sepolia USDC'))
expect(dataentries[1]).toEqual(strToBytes('USDC.s'))
expect(U256.fromBytes(dataentries[2] as Uint8Array)).toBeGreaterThan(0n)
})

test('readStorage with empty keys list', async () => {
const dataentries = await provider.readStorage(USDC, [])
expect(dataentries).toHaveLength(0)
})

test('readStorage with existing and non-existing keys', async () => {
const dataentries = await provider.readStorage(USDC, [
'bad_key',
'NAME',
'bad_key2',
'SYMBOL',
])
expect(dataentries).toHaveLength(4)
expect(dataentries[0]).toBeNull()
expect(dataentries[1]).toEqual(strToBytes('Sepolia USDC'))
expect(dataentries[2]).toBeNull()
expect(dataentries[3]).toEqual(strToBytes('USDC.s'))
})
})
38 changes: 35 additions & 3 deletions test/integration/publicAPI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,13 @@ describe('client tests', () => {

test('getDatastoreEntry', async () => {
const entry = await client.getDatastoreEntry(NAME_KEY, TEST_CONTRACT)
expect(bytesToStr(entry)).toBe(NAME_VAL)
expect(entry).not.toBeNull()
expect(bytesToStr(entry as Uint8Array)).toBe(NAME_VAL)
})

test('getDatastoreEntry speculative', async () => {
const entry = await client.getDatastoreEntry(NAME_KEY, TEST_CONTRACT, false)
expect(bytesToStr(entry)).toBe(NAME_VAL)
expect(bytesToStr(entry as Uint8Array)).toBe(NAME_VAL)
})

test('getDatastoreEntries', async () => {
Expand All @@ -133,10 +134,41 @@ describe('client tests', () => {
address: TEST_CONTRACT,
key: strToBytes(NAME_KEY),
},
]) // retrieve final by default

expect(entries).toHaveLength(1)
expect(entries[0]).not.toBeNull()
expect(bytesToStr(entries[0] as Uint8Array)).toBe(NAME_VAL)
})

test('getDatastoreEntries with bad keys', async () => {
const entries = await client.getDatastoreEntries([
{
address: TEST_CONTRACT,
key: strToBytes('bad_key'),
},
])

expect(entries).toHaveLength(1)
expect(bytesToStr(entries[0])).toBe(NAME_VAL)
expect(entries[0]).toBeNull()
})

test('getDatastoreEntries 2 keys: one good one bad', async () => {
const entries = await client.getDatastoreEntries([
{
address: TEST_CONTRACT,
key: strToBytes('bad_key'),
},
{
address: TEST_CONTRACT,
key: strToBytes(NAME_KEY),
},
])

expect(entries).toHaveLength(2)
expect(entries[0]).toBeNull()
expect(entries[1]).not.toBeNull()
expect(bytesToStr(entries[1] as Uint8Array)).toBe(NAME_VAL)
})

test.skip('sendOperations', async () => {
Expand Down

1 comment on commit 6bf0457

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for experimental massa-web3

St.
Category Percentage Covered / Total
🟡 Statements 65.73% 1264/1923
🔴 Branches 43.04% 201/467
🔴 Functions 45.87% 222/484
🟡 Lines 66.26% 1257/1897

Test suite run success

134 tests passing in 15 suites.

Report generated by 🧪jest coverage report action from 6bf0457

Please sign in to comment.