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

Switching to use ethers and accept RPC url for token gating validation #124

Merged
merged 6 commits into from
Apr 4, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
cache: 'yarn'
- run: |
yarn -v
Expand Down
2 changes: 1 addition & 1 deletion config/tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"lib": ["es2018", "dom"],
"module": "commonjs",
"moduleResolution": "node",
"target": "es5",
"target": "es6",
"strict": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
Expand Down
4 changes: 2 additions & 2 deletions config/tsconfig.sdk.esm.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["es2020", "dom"],
"target": "esnext",
"module": "esnext",
"target": "es2020",
"module": "es2020",
"outDir": "../dist/esm"
},
"include": ["../src/**/*.ts"]
Expand Down
15 changes: 4 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"clean": "npm-run-all -s clean:*",
"clean:test-artifacts": "rimraf coverage",
"clean:build": "rimraf dist",
"clean_node_modules": "rimraf node_modules"
"clean_node_modules": "rimraf node_modules",
"prepare": "husky install"
},
"devDependencies": {
"@ikscodes/eslint-config": "^8.4.1",
Expand All @@ -52,21 +53,13 @@
"ts-jest": "^27.1.3",
"ts-node": "^10.2.0",
"tslint": "~5.20.1",
"typescript": "^5.3.3",
"web3": "^4.6.0"
"typescript": "^5.3.3"
},
"dependencies": {
"ethereum-cryptography": "^1.0.1",
"ethers": "^6.11.1",
"node-fetch": "^2.6.7"
},
"peerDependencies": {
"web3": "^4.6.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx}": "eslint --fix"
},
Expand Down
13 changes: 7 additions & 6 deletions src/modules/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Web3 from 'web3';
import { ethers } from "ethers";
import { BaseModule } from '../base-module';
import {createExpectedBearerStringError} from '../../core/sdk-exceptions';
import { ValidateTokenOwnershipResponse } from '../../types';
Expand All @@ -22,7 +22,7 @@ export class UtilsModule extends BaseModule {
didToken: string,
contractAddress: string,
contractType: 'ERC721' | 'ERC1155',
web3: Web3,
rpcURL: string,
tokenId?: string,
): Promise<ValidateTokenOwnershipResponse> {
// Make sure if ERC1155 has a tokenId
Expand Down Expand Up @@ -56,12 +56,13 @@ export class UtilsModule extends BaseModule {

// Check on-chain if user owns NFT by calling contract with web3
let balance = BigInt(0);
const provider = new ethers.JsonRpcProvider();
if (contractType === 'ERC721') {
const contract = new web3.eth.Contract(ERC721ContractABI, contractAddress);
balance = BigInt(await contract.methods.balanceOf(walletAddress).call());
const contract = new ethers.Contract(contractAddress, ERC721ContractABI, provider);
balance = BigInt(await contract.balanceOf(walletAddress));
} else {
const contract = new web3.eth.Contract(ERC1155ContractABI, contractAddress);
balance = BigInt(await contract.methods.balanceOf(walletAddress, tokenId).call());
const contract = new ethers.Contract(contractAddress, ERC1155ContractABI, provider);
balance = BigInt(await contract.balanceOf(walletAddress, tokenId));
}
if (balance > BigInt(0)) {
return {
Expand Down
57 changes: 5 additions & 52 deletions src/modules/utils/ownershipABIs.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,6 @@
// Reduced ABI for ERC1155 with just balanceOf
export const ERC1155ContractABI = [
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_id",
"type": "uint256"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
];

// Reduced ABI for ERC721 with just balanceOf
export const ERC721ContractABI = [
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_id",
"type": "uint256"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
];
"function balanceOf(address) view returns (uint)",
]
export const ERC1155ContractABI = [
"function balanceOf(address, id) view returns (uint)",
]
74 changes: 28 additions & 46 deletions test/spec/modules/utils/validateTokenOwnership.spec.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
import Web3 from 'web3';
import { createMagicAdminSDK } from '../../../lib/factories';

jest.mock('ethers', () => {
const originalModule = jest.requireActual('ethers');
return {
...originalModule,
ethers: {
...originalModule.ethers,
Contract: jest.fn(() => ({
balanceOf: jest.fn().mockImplementation((walletAddress: string, tokenId?: string) => {
if (tokenId === '2') {
return BigInt(1); // User owns token
} else {
return BigInt(0); // User doesn't own token
}
}),
})),
},
};
});


test('Throws an error if ERC1155 and no token provided', async () => {
const sdk = createMagicAdminSDK('https://example.com');
const web3 = new Web3('https://example.com');

await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', web3)).rejects.toThrow(
await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', 'https://example.com')).rejects.toThrow(
'ERC1155 requires a tokenId',
);
});

test('Returns an error if DID token is malformed', async () => {
const sdk = createMagicAdminSDK('https://example.com');
const web3 = new Web3('https://example.com');

// Mock the magic token validation by setting the code to ERROR_MALFORMED_TOKEN
sdk.token.validate = jest.fn().mockRejectedValue({ code: 'ERROR_MALFORMED_TOKEN' });

await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', web3, '1')).resolves.toEqual({
await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', 'https://example.com', '1')).resolves.toEqual({
valid: false,
error_code: 'UNAUTHORIZED',
message: 'Invalid DID token: ERROR_MALFORMED_TOKEN',
Expand All @@ -26,12 +43,11 @@ test('Returns an error if DID token is malformed', async () => {

test('Returns an error if DID token is expired', async () => {
const sdk = createMagicAdminSDK('https://example.com');
const web3 = new Web3('https://example.com');

// Mock the magic token validation by setting the code to ERROR_DIDT_EXPIRED
sdk.token.validate = jest.fn().mockRejectedValue({ code: 'ERROR_DIDT_EXPIRED' });

await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', web3, '1')).resolves.toEqual({
await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', 'https://example.com', '1')).resolves.toEqual({
valid: false,
error_code: 'UNAUTHORIZED',
message: 'Invalid DID token: ERROR_DIDT_EXPIRED',
Expand All @@ -40,12 +56,11 @@ test('Returns an error if DID token is expired', async () => {

test('Throws an error if DID token validation returns unexpected error code', async () => {
const sdk = createMagicAdminSDK('https://example.com');
const web3 = new Web3('https://example.com');

// Mock the magic token validation by setting the code to ERROR_MALFORMED_TOKEN
sdk.token.validate = jest.fn().mockRejectedValue({ code: 'UNKNOWN' });

await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', web3, '1')).rejects.toThrow();
await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', 'https://example.com', '1')).rejects.toThrow();
});

test('Returns an error if ERC721 token is not owned by user', async () => {
Expand All @@ -55,24 +70,13 @@ test('Returns an error if ERC721 token is not owned by user', async () => {
sdk.token.validate = jest.fn().mockResolvedValue({});
// Mock the getPublicAddress to return valid email and wallet
sdk.token.getPublicAddress = jest.fn().mockReturnValue('0x610dcb8fd5cf7f544b85290889a456916fbeaba2');
// Mock the web3 contract.methods.balanceOf to return 0
const callStub = jest.fn().mockResolvedValue(BigInt(0));
// Mock the contract instance
const contractMock: any = {
methods: {
balanceOf: jest.fn().mockReturnValue({ call: callStub }),
},
};
// Mock web3.eth.Contract
const web3 = new Web3('https://example.com');
jest.spyOn(web3.eth, 'Contract').mockReturnValue(contractMock);

await expect(
sdk.utils.validateTokenOwnership(
'did:ethr:0x123',
'0x610dcb8fd5cf7f544b85290889a456916fbeaba2',
'ERC721',
web3,
'https://example.com',
'1',
),
).resolves.toEqual({
Expand All @@ -89,24 +93,13 @@ test('Returns an error if ERC1155 token is not owned by user', async () => {
sdk.token.validate = jest.fn().mockResolvedValue({});
// Mock the getPublicAddress to return valid email and wallet
sdk.token.getPublicAddress = jest.fn().mockReturnValue('0x610dcb8fd5cf7f544b85290889a456916fbeaba2');
// Mock the web3 contract.methods.balanceOf to return 0
const callStub = jest.fn().mockResolvedValue(BigInt(0));
// Mock the contract instance
const contractMock: any = {
methods: {
balanceOf: jest.fn().mockReturnValue({ call: callStub }),
},
};
// Mock web3.eth.Contract
const web3 = new Web3('https://example.com');
jest.spyOn(web3.eth, 'Contract').mockReturnValue(contractMock);

await expect(
sdk.utils.validateTokenOwnership(
'did:ethr:0x123',
'0x610dcb8fd5cf7f544b85290889a456916fbeaba2',
'ERC1155',
web3,
'https://example.com',
'1',
),
).resolves.toEqual({
Expand All @@ -123,25 +116,14 @@ test('Returns success if ERC1155 token is owned by user', async () => {
sdk.token.validate = jest.fn().mockResolvedValue({});
// Mock the getPublicAddress to return valid email and wallet
sdk.token.getPublicAddress = jest.fn().mockReturnValue('0x610dcb8fd5cf7f544b85290889a456916fbeaba2');
// Mock the web3 contract.methods.balanceOf to return 0
const callStub = jest.fn().mockResolvedValue(BigInt(1));
// Mock the contract instance
const contractMock: any = {
methods: {
balanceOf: jest.fn().mockReturnValue({ call: callStub }),
},
};
// Mock web3.eth.Contract
const web3 = new Web3('https://example.com');
jest.spyOn(web3.eth, 'Contract').mockReturnValue(contractMock);

await expect(
sdk.utils.validateTokenOwnership(
'did:ethr:0x123',
'0x610dcb8fd5cf7f544b85290889a456916fbeaba2',
'ERC1155',
web3,
'1',
'https://example.com',
'2',
),
).resolves.toEqual({
valid: true,
Expand Down
5 changes: 4 additions & 1 deletion test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "../config/tsconfig.test.json"
"extends": "../config/tsconfig.test.json",
"compilerOptions": {
"target": "es5"
}
}
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "./config/tsconfig.base.json"
"extends": "./config/tsconfig.base.json",
"compilerOptions": {
"target": "es5"
}
}
Loading
Loading