Repository: massalabs/massa-web3
Pull request #763: add immutable and fix mns link resolve
Author: login: fleandrei
Created: 2025-05-05T14:17:05Z | Merged: 2025-05-06T14:53:24Z



Diff:
diff --git a/src/deweb/immutable.ts b/src/deweb/immutable.ts
new file mode 100644
index 00000000..005800f4
--- /dev/null
+++ b/src/deweb/immutable.ts
@@ -0,0 +1,44 @@
+import { Provider, PublicProvider } from 'src/provider'
+import { Operation } from 'src/operation'
+import { PublicAPI } from '..'
+import { getPublicApiByChainId } from '../utils/networks'
+
+export async function makeImmutable(
+  address: string,
+  provider: Provider,
+  waitFinal = false
+): Promise<Operation> {
+  const operation = await provider.callSC({
+    func: 'upgradeSC',
+    target: address,
+  })
+
+  await (waitFinal
+    ? operation.waitFinalExecution()
+    : operation.waitSpeculativeExecution())
+  return operation
+}
+
+export async function isImmutable(
+  address: string,
+  provider: PublicProvider,
+  final = false
+): Promise<boolean> {
+  const networkInfo = await provider.networkInfos()
+  let nodeUrl = networkInfo.url
+
+  if (!nodeUrl) {
+    nodeUrl = getPublicApiByChainId(networkInfo.chainId)
+    if (!nodeUrl) {
+      throw new Error(`Unknown network chainId: ${networkInfo.chainId}`)
+    }
+  }
+
+  const client = new PublicAPI(nodeUrl)
+  const bytecode = await client.getAddressesBytecode({
+    address: address,
+    is_final: final, // eslint-disable-line @typescript-eslint/naming-convention
+  })
+
+  return bytecode.length === 0
+}
diff --git a/src/deweb/index.ts b/src/deweb/index.ts
index 6047d47c..442808c9 100644
--- a/src/deweb/index.ts
+++ b/src/deweb/index.ts
@@ -1 +1,2 @@
 export * from './mns'
+export * from './immutable'
diff --git a/src/deweb/mns.ts b/src/deweb/mns.ts
index b7adc02e..4a3e6265 100644
--- a/src/deweb/mns.ts
+++ b/src/deweb/mns.ts
@@ -62,11 +62,11 @@ export async function resolveDeweb(uri: string): Promise<string> {
 
   try {
     const response = await fetch(
-      `${currentProtocol}//${subdomains.slice(1).join('.')}/__deweb_info`
+      `${currentProtocol}//${currentHost}/__deweb_info`
     )
     if (await isDewebInfoData(response)) {
       subdomains[0] = mnsDomain
-      return `${currentProtocol}//${subdomains.join('.')}/${mnsPath}${mnsSearch}`
+      return `${currentProtocol}//${subdomains.join('.')}${mnsPath}${mnsSearch}`
     }
     return defaultUrl
   } catch (error) {
diff --git a/src/utils/networks.ts b/src/utils/networks.ts
index 5743780c..1e06e719 100644
--- a/src/utils/networks.ts
+++ b/src/utils/networks.ts
@@ -34,6 +34,17 @@ export function getNetworkNameByChainId(
   return undefined
 }
 
+export function getPublicApiByChainId(
+  chainId: bigint
+): PublicApiUrl | undefined {
+  for (const [key, value] of Object.entries(CHAIN_ID)) {
+    if (value === chainId) {
+      return PublicApiUrl[key as keyof typeof PublicApiUrl]
+    }
+  }
+  return undefined
+}
+
 export type Network = {
   name: NetworkName | string
   chainId: bigint
diff --git a/test/integration/immutable.spec.ts b/test/integration/immutable.spec.ts
new file mode 100644
index 00000000..2b21f2fb
--- /dev/null
+++ b/test/integration/immutable.spec.ts
@@ -0,0 +1,53 @@
+import { isImmutable } from '../../src/deweb/immutable'
+import { provider, publicProvider } from './setup'
+import { CHAIN_ID } from '../../src/utils/networks'
+import { providerMock } from '../unit/mock/provider.mock'
+
+const immutableDewebAddressBuildnet =
+  'AS12ibbPmE9L4i1D8FC3xP7e9NFtbBWNaP46s6GjnvuiXhWfLmnvW'
+const mutableDewebAddressBuildnet =
+  'AS12oT3yMR4DG89SpweuPa7R1d2sXWvMzGSquVoDgspbiB6nVYUXt'
+
+describe('is immutable', () => {
+  test('should return true for immutable address', async () => {
+    const isImmutableWebsite = await isImmutable(
+      immutableDewebAddressBuildnet,
+      publicProvider
+    )
+    expect(isImmutableWebsite).toBe(true)
+  })
+
+  test('should return false for mutable address', async () => {
+    const isImmutableWebsite = await isImmutable(
+      mutableDewebAddressBuildnet,
+      publicProvider
+    )
+    expect(isImmutableWebsite).toBe(false)
+  })
+
+  test('should work for provider that do not provide node url', async () => {
+    jest.spyOn(providerMock, 'networkInfos').mockResolvedValueOnce({
+      url: undefined,
+      chainId: CHAIN_ID.Buildnet,
+      name: 'Buildnet',
+      minimalFee: 10000000n,
+    })
+    const isImmutableWebsite = await isImmutable(
+      immutableDewebAddressBuildnet,
+      providerMock
+    )
+    expect(isImmutableWebsite).toBe(true)
+  })
+
+  test('if provider return no node url and invalid chainid, should throw error', async () => {
+    jest.spyOn(providerMock, 'networkInfos').mockResolvedValueOnce({
+      url: undefined,
+      chainId: 0n, // invalid chain id
+      name: 'Buildnet',
+      minimalFee: 10000000n,
+    })
+    expect(
+      async () => await isImmutable(immutableDewebAddressBuildnet, providerMock)
+    ).rejects.toThrow('Unknown network chainId: 0')
+  })
+})
diff --git a/test/integration/provider.spec.ts b/test/integration/provider.spec.ts
index 65e90fa6..8b68a5fa 100644
--- a/test/integration/provider.spec.ts
+++ b/test/integration/provider.spec.ts
@@ -1,4 +1,4 @@
-import { CHAIN_ID, GrpcApiUrl, NetworkName } from '../../src'
+import { CHAIN_ID, GrpcApiUrl, PublicApiUrl, NetworkName } from '../../src'
 import { grpcProvider, provider } from './setup'
 import { strToBytes, U256 } from '../../src/basicElements/serializers'
 
@@ -12,7 +12,7 @@ describe('Provider JsonRPC tests', () => {
       name: NetworkName.Buildnet,
       minimalFee: 10000000n,
       chainId: CHAIN_ID.Buildnet,
-      url: 'https://buildnet.massa.net/api/v2',
+      url: PublicApiUrl.Buildnet,
     })
     expect(infos_grpc).toEqual({
       name: NetworkName.Buildnet,
diff --git a/test/unit/mock/provider.mock.ts b/test/unit/mock/provider.mock.ts
index 291c4518..cfff1b08 100644
--- a/test/unit/mock/provider.mock.ts
+++ b/test/unit/mock/provider.mock.ts
@@ -4,4 +4,5 @@ import { blockchainClientMock } from './blockchainClient.mock'
 export const providerMock = {
   getOperationStatus: blockchainClientMock.getOperationStatus,
   getEvents: blockchainClientMock.getEvents,
+  networkInfos: jest.fn().mockResolvedValue({}),
 } as any as Provider
diff --git a/test/unit/networks.spec.ts b/test/unit/networks.spec.ts
new file mode 100644
index 00000000..deb935ea
--- /dev/null
+++ b/test/unit/networks.spec.ts
@@ -0,0 +1,53 @@
+import {
+  getPublicApiByChainId,
+  getNetworkNameByChainId,
+  NetworkName,
+  PublicApiUrl,
+  CHAIN_ID,
+} from '../../src/utils/networks'
+
+describe('getPublicApiByChainId', () => {
+  test('returns correct PublicApiUrl for Mainnet chain ID', () => {
+    const result = getPublicApiByChainId(CHAIN_ID.Mainnet)
+    expect(result).toBe(PublicApiUrl.Mainnet)
+  })
+
+  test('returns correct PublicApiUrl for Testnet chain ID', () => {
+    const result = getPublicApiByChainId(CHAIN_ID.Testnet)
+    expect(result).toBe(PublicApiUrl.Testnet)
+  })
+
+  test('returns correct PublicApiUrl for Buildnet chain ID', () => {
+    const result = getPublicApiByChainId(CHAIN_ID.Buildnet)
+    expect(result).toBe(PublicApiUrl.Buildnet)
+  })
+
+  test('returns undefined for an unknown chain ID', () => {
+    const unknownChainId = 12345678n
+    const result = getPublicApiByChainId(unknownChainId)
+    expect(result).toBeUndefined()
+  })
+})
+
+describe('getNetworkNameByChainId', () => {
+  test('returns correct NetworkName for Mainnet chain ID', () => {
+    const result = getNetworkNameByChainId(CHAIN_ID.Mainnet)
+    expect(result).toBe(NetworkName.Mainnet)
+  })
+
+  test('returns correct NetworkName for Testnet chain ID', () => {
+    const result = getNetworkNameByChainId(CHAIN_ID.Testnet)
+    expect(result).toBe(NetworkName.Testnet)
+  })
+
+  test('returns correct NetworkName for Buildnet chain ID', () => {
+    const result = getNetworkNameByChainId(CHAIN_ID.Buildnet)
+    expect(result).toBe(NetworkName.Buildnet)
+  })
+
+  test('returns undefined for an unknown chain ID', () => {
+    const unknownChainId = 12345678n
+    const result = getNetworkNameByChainId(unknownChainId)
+    expect(result).toBeUndefined()
+  })
+})


Reviews:
user: login: peterjah
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T15:04:27Z
user: login: peterjah
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T15:05:09Z
user: login: peterjah
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-05T15:05:16Z
user: login: peterjah
body: Nicey !!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-06T13:48:16Z
user: login: peterjah
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T13:49:25Z
user: login: peterjah
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-06T14:42:21Z

Review comments:
diff_hunk: @@ -0,0 +1,32 @@
+import { Provider } from 'src/provider'
+import { Operation } from 'src/operation'
+import { PublicAPI } from '../client'
+
+export async function makeImmutable(
+  address: string,
+  provider: Provider,
+  waitFinal = false
+): Promise<Operation> {
+  const operation = await provider.callSC({
+    func: 'upgradeSC',
+    target: address,
+  })
+
+  await (waitFinal
+    ? operation.waitFinalExecution()
+    : operation.waitSpeculativeExecution())
+  return operation
+}
+
+export async function isImmutable(
+  address: string,
+  nodeUrl: string,
path: src/deweb/immutable.ts
user: login: peterjah
body: its strange to provide an url here.
It would best to use the provider in function parameter, and get the url from it ( using networkInfos). you can also handle the case where nodeUrl is not available (its the case for bearby) and use the public one based on the chain ID
created_at: 2025-05-05T15:04:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,32 @@
+import { Provider } from 'src/provider'
+import { Operation } from 'src/operation'
+import { PublicAPI } from '../client'
+
+export async function makeImmutable(
+  address: string,
+  provider: Provider,
+  waitFinal = false
+): Promise<Operation> {
+  const operation = await provider.callSC({
+    func: 'upgradeSC',
+    target: address,
+  })
+
+  await (waitFinal
+    ? operation.waitFinalExecution()
+    : operation.waitSpeculativeExecution())
+  return operation
+}
+
+export async function isImmutable(
+  address: string,
+  nodeUrl: string,
+  waitFinal = false
path: src/deweb/immutable.ts
user: login: peterjah
body: i would propose rename`waitFinal` => `final`, there is no waiting here
created_at: 2025-05-05T15:05:09Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+import { Provider, PublicProvider } from 'src/provider'
+import { Operation } from 'src/operation'
+import { PublicAPI } from '..'
+import { getPublicApiByChainId } from '../utils/networks'
+
+export async function makeImmutable(
+  address: string,
+  provider: Provider,
+  waitFinal = false
+): Promise<Operation> {
+  const operation = await provider.callSC({
+    func: 'upgradeSC',
+    target: address,
+  })
+
+  await (waitFinal
+    ? operation.waitFinalExecution()
+    : operation.waitSpeculativeExecution())
+  return operation
+}
+
+export async function isImmutable(
+  address: string,
+  provider: PublicProvider,
+  final = false
+): Promise<boolean> {
+  const networkInfo = await provider.networkInfos()
+  let nodeUrl = networkInfo.url
+
+  if (!nodeUrl) {
+    nodeUrl = getPublicApiByChainId(networkInfo.chainId)
+    if (!nodeUrl) {
+      throw new Error(`Unknown network chainId: ${networkInfo.chainId}`)
+    }
+  }
+
+  console.log('nodeUrl', nodeUrl)
path: src/deweb/immutable.ts
user: login: peterjah
body: console.log to delete
created_at: 2025-05-06T13:49:24Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-05T14:22:46Z
body: <!-- jest coverage report action for options with hash 68ff960c11933d900785837f26494f1d -->

## Coverage report for experimental massa-web3


| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category | Percentage | Covered / Total |
| :-: | :- | :- | :-: |
| 🟡 | Statements | <div title="Base coverage is: 59.99%">60.06% (+0.06% 🔼)</div> | 1508/2511 |
| 🔴 | Branches | <div title="Base coverage is: 31.57%">31.67% (+0.1% 🔼)</div> | 260/821 |
| 🔴 | Functions | <div title="Base coverage is: 41.14%">41.28% (+0.14% 🔼)</div> | 225/545 |
| 🟡 | Lines | <div title="Base coverage is: 60.5%">60.56% (+0.06% 🔼)</div> | 1503/2482 |

<details><summary>Show new covered files 🐣</summary>
<br/>

| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | File | Statements | Branches | Functions | Lines |
| :-: | :- | :- | :- | :- | :- |
| 🔴 | deweb/immutable.ts | 33.33% | 0% | 0% | 33.33% |

</details>


## Test suite run success
143 tests passing in 16 suites.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from 4420a513bf8b98a43862859d4699b2f966799bea</p>

author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-06T14:03:56Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=massalabs_massa-web3&pullRequest=763) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=massalabs_massa-web3&pullRequest=763&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=massalabs_massa-web3&pullRequest=763&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=massalabs_massa-web3&pullRequest=763&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=massalabs_massa-web3&pullRequest=763&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=massalabs_massa-web3&pullRequest=763&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=massalabs_massa-web3&pullRequest=763)


author_association: NONE