Skip to content

Commit

Permalink
Adding web3 package and code for token validation
Browse files Browse the repository at this point in the history
  • Loading branch information
bengriffin1 committed Mar 8, 2024
1 parent 38a7577 commit 4a361bd
Show file tree
Hide file tree
Showing 5 changed files with 709 additions and 3 deletions.
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,
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.',
};
}
}
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';
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

0 comments on commit 4a361bd

Please sign in to comment.