Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "refactor!: remove supportsInterface" #359

Merged
merged 1 commit into from
Nov 29, 2023
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
82 changes: 82 additions & 0 deletions docs/classes/ERC725.md
Original file line number Diff line number Diff line change
Expand Up @@ -1571,3 +1571,85 @@ await myErc725.isValidSignature(
[lsp6 keymanager permissions]: ../../../../../standards/universal-profile/lsp6-key-manager#permissions
[lsp6 keymanager standard]: https://docs.lukso.tech/standards/universal-profile/lsp6-key-manager
[lsp-2 erc725yjsonschema]: https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md

## supportsInterface

```js
myERC725.supportsInterface(interfaceIdOrName);
```

```js
ERC725.supportsInterface(interfaceIdOrName, options);
```

You can use this function if you need to check if the ERC725 object or a smart contract supports a specific interface (by ID or name). When you use the function on your instantiated ERC725 class, it will use the contract address and provider provided at instantiation. On non instantiated class, you need to specify them in the `options` parameter.

:::caution
The `interfaceId` is not the most secure way to check for a standard, as they could be set manually.
:::

#### Parameters

##### 1. `interfaceIdOrName` - String

Either a string of the hexadecimal `interfaceID` as defined by [ERC165](https://eips.ethereum.org/EIPS/eip-165) or one of the predefined interface names:

| interfaceName | Standard |
| :------------------------------ | :------------------------------------------------------------------------------------------------------------------------- |
| `ERC1271` | [EIP-1271: Standard Signature Validation Method for Contracts](https://eips.ethereum.org/EIPS/eip-1271) |
| `ERC725X` | [EIP-725: General execution standard](https://eips.ethereum.org/EIPS/eip-725) |
| `ERC725Y` | [EIP-725: General key-value store](https://eips.ethereum.org/EIPS/eip-725) |
| `LSP0ERC725Account` | [LSP-0: ERC725 Account](https://docs.lukso.tech/standards/universal-profile/lsp0-erc725account) |
| `LSP1UniversalReceiver` | [LSP-1: Universal Receiver](https://docs.lukso.tech/standards/generic-standards/lsp1-universal-receiver) |
| `LSP1UniversalReceiverDelegate` | [LSP-1: Universal Receiver Delegate](https://docs.lukso.tech/standards/universal-profile/lsp1-universal-receiver-delegate) |
| `LSP6KeyManager` | [LSP-6: Key Manager](https://docs.lukso.tech/standards/universal-profile/lsp6-key-manager) |
| `LSP7DigitalAsset` | [LSP-7: Digital Asset](https://docs.lukso.tech/standards/nft-2.0/LSP7-Digital-Asset) |
| `LSP8IdentifiableDigitalAsset` | [LSP-8: Identifiable Digital Asset](https://docs.lukso.tech/standards/nft-2.0/LSP8-Identifiable-Digital-Asset) |
| `LSP9Vault` | [LSP-9: Vault](https://docs.lukso.tech/standards/universal-profile/lsp9-vault) |

:::info

The `interfaceName` will only check for the latest version of the standard's `interfaceID`, which can be found in `src/constants/interfaces`. For LSPs, the `interfaceIDs` are taken from the latest release of the [@lukso/lsp-smart-contracts](https://github.com/lukso-network/lsp-smart-contracts) library.

:::info

##### 2. `options` - Object (optional)

On non instantiated class, you should provide an `options` object.

| Name | Type | Description |
| :-------- | :----- | :------------------------------------------------------------------- |
| `address` | string | Address of the smart contract to check against a certain interface. |
| `rpcUrl` | string | RPC URL to connect to the network the smart contract is deployed to. |
| `gas` | number | Optional: gas parameter to use. Default: 1_000_000. |

#### Returns

| Type | Description |
| :----------------- | :------------------------------------------------------------ |
| `Promise<boolean>` | Returns `true` if the interface was found, otherwise `false`. |

#### Examples

```javascript title="By using the interface ID"
myErc725.supportsInterface('0xfd4d5c50');
// true

ERC725.supportsInterface('0xfd4d5c50', {
address: '0xe408BDDbBAB1985006A2c481700DD473F932e5cB',
rpcUrl: 'https://rpc.testnet.lukso.network',
});
// false
```

```javascript title="By using interface name"
myErc725.supportsInterface('LSP0ERC725Account');
// false

ERC725.supportsInterface('LSP0ERC725Account', {
address: '0x0Dc07C77985fE31996Ed612F568eb441afe5768D',
rpcUrl: 'https://rpc.testnet.lukso.network',
gas: 20_000_000,
});
// true
```
39 changes: 39 additions & 0 deletions src/constants/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
This file is part of @erc725/erc725.js.
@erc725/erc725.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
@erc725/erc725.js is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with @erc725/erc725.js. If not, see <http://www.gnu.org/licenses/>.
*/

// from @lukso/lsp-smart-contracts v0.12.0, erc725.js should stay independent
export const INTERFACE_IDS_0_12_0 = {
ERC1271: '0x1626ba7e',
ERC725X: '0x7545acac',
ERC725Y: '0x629aa694',
LSP0ERC725Account: '0x24871b3d',
LSP1UniversalReceiver: '0x6bb56a14',
LSP1UniversalReceiverDelegate: '0xa245bbda',
LSP6KeyManager: '0x23f34c62',
LSP7DigitalAsset: '0xdaa746b7',
LSP8IdentifiableDigitalAsset: '0x30dc5278',
LSP9Vault: '0x28af17e6',
LSP11BasicSocialRecovery: '0x049a28f1',
LSP14Ownable2Step: '0x94be5999',
LSP17Extendable: '0xa918fa6b',
LSP17Extension: '0xcee78b40',
LSP20CallVerification: '0x1a0eb6a5',
LSP20CallVerifier: '0x0d6ecac7',
LSP25ExecuteRelayCall: '0x5ac79908',
};

export interface AddressProviderOptions {
address: string;
provider: any;
}
39 changes: 39 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
SUPPORTED_VERIFICATION_METHOD_STRINGS,
} from './constants/constants';
import { decodeKey } from './lib/decodeData';
import { INTERFACE_IDS_0_12_0 } from './constants/interfaces';

const address = '0x0c03fba782b07bcf810deb3b7f0595024a444f4e';

Expand Down Expand Up @@ -1389,6 +1390,44 @@ describe('encodeKeyName', () => {
});
});

describe('supportsInterface', () => {
const erc725Instance = new ERC725([]);

it('is available on instance and class', () => {
assert.typeOf(ERC725.supportsInterface, 'function');
assert.typeOf(erc725Instance.supportsInterface, 'function');
});

const interfaceId = INTERFACE_IDS_0_12_0.LSP1UniversalReceiver;
const rpcUrl = 'https://my.test.provider';
const contractAddress = '0xcafecafecafecafecafecafecafecafecafecafe';

it('should throw when provided address is not an address', async () => {
try {
await ERC725.supportsInterface(interfaceId, {
address: 'notAnAddress',
rpcUrl,
});
} catch (error: any) {
assert.deepStrictEqual(error.message, 'Invalid address');
}
});

it('should throw when rpcUrl is not provided on non instantiated class', async () => {
try {
await ERC725.supportsInterface(interfaceId, {
address: contractAddress,
// @ts-ignore
rpcUrl: undefined,
});
} catch (error: any) {
assert.deepStrictEqual(error.message, 'Missing RPC URL');
}
});

// TODO: add test to test the actual behavior of the function.
});

describe('checkPermissions', () => {
const erc725Instance = new ERC725([]);

Expand Down
46 changes: 45 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ import { decodeData } from './lib/decodeData';
import { getDataFromExternalSources } from './lib/getDataFromExternalSources';
import { DynamicKeyPart, DynamicKeyParts } from './types/dynamicKeys';
import { getData } from './lib/getData';
import { checkPermissions } from './lib/detector';
import { decodeValueType, encodeValueType } from './lib/encoder';
import { supportsInterface, checkPermissions } from './lib/detector';
import { decodeMappingKey } from './lib/decodeMappingKey';

export {
Expand Down Expand Up @@ -585,6 +585,50 @@ export class ERC725 {
return decodeMappingKey(keyHash, keyNameOrSchema);
}

/**
* Check if the ERC725 object supports
* a certain interface.
*
* @param interfaceIdOrName Interface ID or supported interface name.
* @returns {Promise<boolean>} if interface is supported.
*/
async supportsInterface(interfaceIdOrName: string): Promise<boolean> {
const { address, provider } = this.getAddressAndProvider();

return supportsInterface(interfaceIdOrName, {
address,
provider,
});
}

/**
* Check if a smart contract address
* supports a certain interface.
*
* @param {string} interfaceIdOrName Interface ID or supported interface name.
* @param options Object of address, RPC URL and optional gas.
* @returns {Promise<boolean>} if interface is supported.
*/
static async supportsInterface(
interfaceIdOrName: string,
options: { address: string; rpcUrl: string; gas?: number },
): Promise<boolean> {
if (!isAddress(options.address)) {
throw new Error('Invalid address');
}
if (!options.rpcUrl) {
throw new Error('Missing RPC URL');
}

return supportsInterface(interfaceIdOrName, {
address: options.address,
provider: this.initializeProvider(
options.rpcUrl,
options?.gas ? options?.gas : DEFAULT_GAS_VALUE,
),
});
}

/**
* Check if the required permissions are included in the granted permissions as defined by the LSP6 KeyManager Standard.
*
Expand Down
44 changes: 43 additions & 1 deletion src/lib/detector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,56 @@
*/
/**
* @file lib/detector.test.ts
* @author Hugo Masclet <@Hugoo>
* @author Felix Hildebrandt <@fhildeb>
* @date 2022
*/

/* eslint-disable no-unused-expressions */

import { expect } from 'chai';
import * as sinon from 'sinon';
import { INTERFACE_IDS_0_12_0 } from '../constants/interfaces';

import { checkPermissions } from './detector';
import { supportsInterface, checkPermissions } from './detector';

describe('supportsInterface', () => {
it('it should return true if the contract supports the interface with name', async () => {
const contractAddress = '0xcafecafecafecafecafecafecafecafecafecafe';
const interfaceName = 'LSP0ERC725Account';

const providerStub = { supportsInterface: sinon.stub() };

providerStub.supportsInterface
.withArgs(contractAddress, INTERFACE_IDS_0_12_0[interfaceName])
.returns(Promise.resolve(true));

const doesSupportInterface = await supportsInterface(interfaceName, {
address: contractAddress,
provider: providerStub,
});

expect(doesSupportInterface).to.be.true;
});

it('it should return true if the contract supports the interface with interfaceId', async () => {
const contractAddress = '0xcafecafecafecafecafecafecafecafecafecafe';
const interfaceId = INTERFACE_IDS_0_12_0.LSP1UniversalReceiver;

const providerStub = { supportsInterface: sinon.stub() };

providerStub.supportsInterface
.withArgs(contractAddress, interfaceId)
.returns(Promise.resolve(true));

const doesSupportInterface = await supportsInterface(interfaceId, {
address: contractAddress,
provider: providerStub,
});

expect(doesSupportInterface).to.be.true;
});
});

describe('checkPermissions', () => {
describe('test with single permission', () => {
Expand Down
36 changes: 36 additions & 0 deletions src/lib/detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,47 @@

/**
* @file detector.ts
* @author Hugo Masclet <@Hugoo>
* @author Felix Hildebrandt <@fhildeb>
* @date 2022
*/

import { LSP6_DEFAULT_PERMISSIONS } from '../constants/constants';

import {
AddressProviderOptions,
INTERFACE_IDS_0_12_0,
} from '../constants/interfaces';

/**
* Check if a smart contract address
* supports a certain interface.
*
* @param {string} interfaceId Interface ID or supported interface name.
* @param options Object with address and RPC URL.
* @returns {Promise<boolean>} if interface is supported.
*/
export const supportsInterface = async (
interfaceIdOrName: string,
options: AddressProviderOptions,
): Promise<boolean> => {
let plainInterfaceId: string;
if (INTERFACE_IDS_0_12_0[interfaceIdOrName]) {
plainInterfaceId = INTERFACE_IDS_0_12_0[interfaceIdOrName];
} else {
plainInterfaceId = interfaceIdOrName;
}

try {
return await options.provider.supportsInterface(
options.address,
plainInterfaceId,
);
} catch (error) {
throw new Error(`Error checking the interface: ${error}`);
}
};

/**
* @notice Check if the given string is a valid 32-byte hex string.
* @param str The string to be checked.
Expand Down
Loading