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

Adding type and abis #122

Merged
merged 10 commits into from
Apr 3, 2024
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
},
"dependencies": {
"ethereum-cryptography": "^1.0.1",
"node-fetch": "^2.6.7"
"node-fetch": "^2.6.7",
"web3": "^4.6.0"
},
"husky": {
"hooks": {
Expand Down
61 changes: 61 additions & 0 deletions src/modules/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Web3 from 'web3';
import { BaseModule } from '../base-module';
import { createExpectedBearerStringError } from '../../core/sdk-exceptions';
import { ValidateTokenOwnershipResponse } from '../../types';
import { ERC1155ContractABI, ERC721ContractABI } from './ownershipABIs';

export class UtilsModule extends BaseModule {
/**
Expand All @@ -12,4 +15,62 @@ export class UtilsModule extends BaseModule {

return header.substring(7);
}

// Token Gating function validates user ownership of wallet + NFT
public async validateTokenOwnership(
didToken: string,
contractAddress: string,
contractType: 'ERC721' | 'ERC1155',
web3: Web3,
Copy link
Member

Choose a reason for hiding this comment

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

Curious, why web3 needs to be a param in this case?

Copy link
Member

Choose a reason for hiding this comment

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

Just a thought, maybe web3 can be a peer dependency, which can keep our admin sdk light-weight and thin

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made it a param to make the mocks more straightforward in the unit tests, and also to allow the users to pass in web3 instances that may have custom options.

I'm not familiar with peer dependencies, looking into it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved into peer dependencies @Ethella

tokenId?: string,
): Promise<ValidateTokenOwnershipResponse> {
// Make sure if ERC1155 has a tokenId
if (contractType === 'ERC1155' && !tokenId) {
throw new Error('ERC1155 requires a tokenId');
}
// Call magic and validate DID token
try {
await this.sdk.token.validate(didToken);
} catch (e) {
// Check if code is malformed token
if (e.code === 'ERROR_MALFORMED_TOKEN') {
return {
valid: false,
error_code: 'UNAUTHORIZED',
message: 'Invalid DID token: ERROR_MALFORMED_TOKEN',
};
}
throw new Error(e.code);
}
const { email, publicAddress: walletAddress } = await this.sdk.users.getMetadataByToken(didToken);
if (!email || !walletAddress) {
return {
valid: false,
error_code: 'UNAUTHORIZED',
message: 'Invalid DID token. May be expired or malformed.',
};
}

// Check on-chain if user owns NFT by calling contract with web3
let balance = BigInt(0);
if (contractType === 'ERC721') {
const contract = new web3.eth.Contract(ERC721ContractABI, contractAddress);
balance = BigInt(await contract.methods.balanceOf(walletAddress).call());
} else {
const contract = new web3.eth.Contract(ERC1155ContractABI, contractAddress);
balance = BigInt(await contract.methods.balanceOf(walletAddress, tokenId).call());
}
if (balance > BigInt(0)) {
return {
valid: true,
error_code: '',
message: '',
};
}
return {
valid: false,
error_code: 'NO_OWNERSHIP',
message: 'User does not own this token.',
};
}
}
53 changes: 53 additions & 0 deletions src/modules/utils/ownershipABIs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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"
}
];
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './didt-types';
export * from './exception-types';
export * from './sdk-types';
export * from './wallet-types';
export * from './utils-types';
5 changes: 5 additions & 0 deletions src/types/utils-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface ValidateTokenOwnershipResponse {
valid: boolean;
error_code: string;
message: string;
}
161 changes: 161 additions & 0 deletions test/spec/modules/utils/validateTokenOwnership.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import Web3 from 'web3';
import { createMagicAdminSDK } from '../../../lib/factories';

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(
'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({
valid: false,
error_code: 'UNAUTHORIZED',
message: 'Invalid DID token: ERROR_MALFORMED_TOKEN',
});
});

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(
'UNKNOWN',
);
});

test('Returns an error if getMetadataByToken doesnt return email or wallet', async () => {
const sdk = createMagicAdminSDK('https://example.com');
const web3 = new Web3('https://example.com');

// Mock the magic token validation to return ok
sdk.token.validate = jest.fn().mockResolvedValue({});
// Mock the getMetadataByToken to return empty email and wallet
sdk.users.getMetadataByToken = jest.fn().mockResolvedValue({ email: null, publicAddress: null });

await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', web3, '1')).resolves.toEqual({
valid: false,
error_code: 'UNAUTHORIZED',
message: 'Invalid DID token. May be expired or malformed.',
});
});

test('Returns an error if ERC721 token is not owned by user', async () => {
const sdk = createMagicAdminSDK('https://example.com');

// Mock the magic token validation to return ok
sdk.token.validate = jest.fn().mockResolvedValue({});
// Mock the getMetadataByToken to return valid email and wallet
sdk.users.getMetadataByToken = jest
.fn()
.mockResolvedValue({ email: '[email protected]', publicAddress: '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,
'1',
),
).resolves.toEqual({
valid: false,
error_code: 'NO_OWNERSHIP',
message: 'User does not own this token.',
});
});

test('Returns an error if ERC1155 token is not owned by user', async () => {
const sdk = createMagicAdminSDK('https://example.com');

// Mock the magic token validation to return ok
sdk.token.validate = jest.fn().mockResolvedValue({});
// Mock the getMetadataByToken to return valid email and wallet
sdk.users.getMetadataByToken = jest
.fn()
.mockResolvedValue({ email: '[email protected]', publicAddress: '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,
'1',
),
).resolves.toEqual({
valid: false,
error_code: 'NO_OWNERSHIP',
message: 'User does not own this token.',
});
});

test('Returns success if ERC1155 token is owned by user', async () => {
const sdk = createMagicAdminSDK('https://example.com');

// Mock the magic token validation to return ok
sdk.token.validate = jest.fn().mockResolvedValue({});
// Mock the getMetadataByToken to return valid email and wallet
sdk.users.getMetadataByToken = jest
.fn()
.mockResolvedValue({ email: '[email protected]', publicAddress: '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',
),
).resolves.toEqual({
valid: true,
error_code: '',
message: '',
});
});
Loading
Loading