Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions clients/tfchain-client-js/lib/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ async function getEntity (self, id) {
const entity = await self.api.query.tfgridModule.entities(id)

const res = entity.toJSON()
if (res.id !== id) {
// A non-existent entity decodes to null; guard before reading fields so we
// throw a clean "No such entity" instead of a TypeError on null.
if (!res || res.id !== id) {
throw Error('No such entity')
}

Expand All @@ -39,15 +41,20 @@ async function getEntity (self, id) {
}

async function getEntityIDByName (self, name) {
const entity = await self.api.query.tfgridModule.entitiesByNameID(name)
// Storage was renamed on the current runtime: entitiesByNameID -> entityIdByName.
const entity = await self.api.query.tfgridModule.entityIdByName(name)

return entity.toJSON()
// Normalize not-found to 0 (the storage may decode to null), preserving the
// "0 means absent" contract that callers (e.g. activation-service) rely on.
return entity.toJSON() || 0
}

async function getEntityIDByPubkey (self, pubkey) {
const entity = await self.api.query.tfgridModule.entitiesByPubkeyID(pubkey)
// Storage was renamed on the current runtime: entitiesByPubkeyID -> entityIdByAccountID.
const entity = await self.api.query.tfgridModule.entityIdByAccountID(pubkey)

return entity.toJSON()
// Normalize not-found to 0 (decodes to null), preserving the "0 means absent" contract.
return entity.toJSON() || 0
}

async function listEntities (self) {
Expand Down
46 changes: 28 additions & 18 deletions clients/tfchain-client-js/lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,31 +63,41 @@ async function getNode (self, id) {
const node = await self.api.query.tfgridModule.nodes(id)

const res = node.toJSON()
if (res.id !== id) {
// A non-existent node decodes to null; guard before reading fields so we throw
// a clean "No such node" instead of a TypeError on null.
if (!res || res.id !== id) {
throw Error('No such node')
}

// Decode location
// Decode location. On the current runtime city/country/latitude/longitude are
// all nested under `location` and byte-encoded. The previous code decoded only
// lat/long and read `res.country`/`res.city` at the top level — where they do
// not exist — so city/country were left hex-encoded.
const { location } = res
const { longitude = '', latitude = '' } = location
location.longitude = hex2a(longitude)
location.latitude = hex2a(latitude)

if (res.country) {
res.country = hex2a(res.country)
}

if (res.city) {
res.city = hex2a(res.city)
if (location) {
location.longitude = hex2a(location.longitude)
location.latitude = hex2a(location.latitude)
location.city = hex2a(location.city)
location.country = hex2a(location.country)
}

const { public_config: publicConfig } = res
// On the current runtime the field decodes as `publicConfig` (camelCase) with a
// nested shape: { ip4: { ip, gw }, ip6: { ip, gw }, domain }. The previous code
// read `public_config` (snake) with a flat { ipv4, ipv6, gw4, gw6 } shape, so it
// matched nothing and left everything hex-encoded.
const { publicConfig } = res
if (publicConfig) {
const { ipv4, ipv6, gw4, gw6 } = publicConfig
publicConfig.ipv4 = hex2a(ipv4)
publicConfig.ipv6 = hex2a(ipv6)
publicConfig.gw4 = hex2a(gw4)
publicConfig.gw6 = hex2a(gw6)
if (publicConfig.ip4) {
publicConfig.ip4.ip = hex2a(publicConfig.ip4.ip)
publicConfig.ip4.gw = hex2a(publicConfig.ip4.gw)
}
if (publicConfig.ip6) {
publicConfig.ip6.ip = hex2a(publicConfig.ip6.ip)
publicConfig.ip6.gw = hex2a(publicConfig.ip6.gw)
}
if (publicConfig.domain) {
publicConfig.domain = hex2a(publicConfig.domain)
}
}

if (res.serialNumber) {
Expand Down
10 changes: 9 additions & 1 deletion clients/tfchain-client-js/lib/pricing_policy.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
const { hex2a } = require('./util')

async function getPricingPolicyById (self, policyId) {
const value = await self.api.query.tfgridModule.pricingPolicies(policyId)

return value.toJSON()
const res = value.toJSON()
// The policy `name` is byte-encoded; decode it to a string. A non-existent
// policy decodes to null, so guard before touching fields.
if (res && res.name) {
res.name = hex2a(res.name)
}
return res
}
module.exports = {
getPricingPolicyById
Expand Down
15 changes: 7 additions & 8 deletions clients/tfchain-client-js/lib/twin.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const { hex2a } = require('./util')

// createTwin creates an entity with given name
async function createTwin (self, relay, pk, callback) {
const create = self.api.tx.tfgridModule.createTwin(relay, pk)
Expand Down Expand Up @@ -49,8 +47,11 @@ async function getTwin (self, id) {
const twin = await self.api.query.tfgridModule.twins(id)

const res = twin.toJSON()
res.ip = hex2a(res.ip)
if (res.id !== id) {
// The Twin struct no longer has an `ip` field (migrated to `relay`/`pk` long
// ago). Return the metadata-decoded object as-is; decoding a non-existent
// field threw `Cannot read properties of undefined` on the current runtime (#1090).
// A non-existent twin decodes to null — guard before reading `id`.
if (!res || res.id !== id) {
throw Error('No such twin')
}
return res
Expand All @@ -69,10 +70,8 @@ async function listTwins (self) {
const twins = await self.api.query.tfgridModule.twins.entries()

const parsedTwins = twins.map(twin => {
const parsedTwin = twin[1].toJSON()
parsedTwin.ip = hex2a(parsedTwin.ip)

return parsedTwin
// See getTwin: no `ip` field on the current runtime — return as-is (#1090).
return twin[1].toJSON()
})

return parsedTwins
Expand Down
5 changes: 5 additions & 0 deletions clients/tfchain-client-js/lib/util.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
function hex2a (hex) {
// Guard against absent/null fields. On the current runtime several previously
// byte-encoded fields are Option<...> (decoded as null) or were removed
// entirely, so callers may pass undefined/null. Returning '' keeps the read
// wrappers from throwing `Cannot read properties of undefined`.
if (hex === undefined || hex === null) return ''
let str = ''
for (let i = 0; i < hex.length; i += 2) {
const v = parseInt(hex.substr(i, 2), 16)
Expand Down
2 changes: 1 addition & 1 deletion clients/tfchain-client-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "API client for the TF Grid",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node --test test/"
},
"author": "",
"license": "ISC",
Expand Down
27 changes: 27 additions & 0 deletions clients/tfchain-client-js/test/util.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { test } = require('node:test')
const assert = require('node:assert')
const { hex2a } = require('../lib/util')

test('hex2a decodes a 0x-prefixed hex string to ASCII', () => {
// "test" = 0x74657374
assert.strictEqual(hex2a('0x74657374'), 'test')
})

test('hex2a decodes a bare (no-0x) hex string to ASCII', () => {
assert.strictEqual(hex2a('74657374'), 'test')
})

test('hex2a returns empty string for undefined (absent field on current runtime)', () => {
// Regression for #1090: twin/node/farm read wrappers call hex2a on fields
// that no longer exist on the current runtime; this must not throw.
assert.strictEqual(hex2a(undefined), '')
})

test('hex2a returns empty string for null (Option field decoded as null)', () => {
assert.strictEqual(hex2a(null), '')
})

test('hex2a returns empty string for empty input', () => {
assert.strictEqual(hex2a(''), '')
assert.strictEqual(hex2a('0x'), '')
})
Loading