Skip to content

Commit

Permalink
Merge pull request #122 from magiclabs/bengriffin1-add-token-gating
Browse files Browse the repository at this point in the history
Adding type and abis
  • Loading branch information
bengriffin1 authored Apr 3, 2024
2 parents ddc0194 + 78dc282 commit 75113c8
Show file tree
Hide file tree
Showing 15 changed files with 2,950 additions and 888 deletions.
6 changes: 5 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
"no-cond-assign": 0,
"class-methods-use-this": 0,
"no-underscore-dangle": 0,
"no-useless-constructor": 0
"no-useless-constructor": 0,
// Note: you must disable the base rule as it can report incorrect errors
"no-shadow": 0,
"@typescript-eslint/no-shadow": "warn",
"no-empty-function": 0
},
"settings": {
"import/resolver": {
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20.10
29 changes: 15 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,38 +29,39 @@
"clean_node_modules": "rimraf node_modules"
},
"devDependencies": {
"@ikscodes/eslint-config": "^6.2.0",
"@ikscodes/prettier-config": "^1.0.0",
"@ikscodes/eslint-config": "^8.4.1",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/jest": "^27.4.1",
"@types/node": "^13.1.2",
"@types/node-fetch": "^2.5.4",
"@typescript-eslint/eslint-plugin": "^2.15.0",
"auto": "^9.60.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"auto": "11.0.5",
"boxen-cli": "^1.0.0",
"esbuild": "^0.14.54",
"eslint": "^6.7.2",
"eslint": "^8.56.0",
"eslint-import-resolver-typescript": "^2.0.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.15.1",
"eslint-plugin-react-hooks": "^1.7.0",
"husky": "^4.2.3",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"husky": "^8.0.3",
"jest": "^27.5.1",
"lint-staged": "^10.0.8",
"npm-run-all": "~4.1.5",
"prettier": "^1.19.1",
"prettier": "^3.2.4",
"rimraf": "~3.0.0",
"ts-jest": "^27.1.3",
"ts-node": "~8.5.2",
"ts-node": "^10.2.0",
"tslint": "~5.20.1",
"typescript": "~3.8.3"
"typescript": "^5.3.3",
"web3": "^4.6.0"
},
"dependencies": {
"ethereum-cryptography": "^1.0.1",
"node-fetch": "^2.6.7"
},
"peerDependencies": {
"web3": "^4.6.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
Expand Down
6 changes: 5 additions & 1 deletion src/core/sdk-exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { ErrorCode } from '../types';
export class MagicAdminSDKError extends Error {
__proto__ = Error;

constructor(public code: ErrorCode, message: string, public data: any[] = []) {
constructor(
public code: ErrorCode,
message: string,
public data: any[] = [],
) {
super(`Magic Admin SDK Error: [${code}] ${message}`);
Object.setPrototypeOf(this, MagicAdminSDKError.prototype);
}
Expand Down
7 changes: 5 additions & 2 deletions src/core/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createApiKeyMissingError } from './sdk-exceptions';
import { TokenModule } from '../modules/token';
import { UsersModule } from '../modules/users';
import { UtilsModule } from '../modules/utils';
import { MagicAdminSDKAdditionalConfiguration } from '../types';
import { get } from '../utils/rest';
import { createApiKeyMissingError } from './sdk-exceptions';

export class MagicAdminSDK {
public readonly apiBaseUrl: string;
Expand Down Expand Up @@ -35,7 +35,10 @@ export class MagicAdminSDK {
* @param secretApiKey
* @param options
*/
constructor(public readonly secretApiKey?: string, options?: MagicAdminSDKAdditionalConfiguration) {
constructor(
public readonly secretApiKey?: string,
options?: MagicAdminSDKAdditionalConfiguration,
) {
const endpoint = options?.endpoint ?? 'https://api.magic.link';
this.apiBaseUrl = endpoint.replace(/\/+$/, '');
this.clientId = options?.clientId ?? null;
Expand Down
66 changes: 65 additions & 1 deletion src/modules/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import Web3 from 'web3';
import { BaseModule } from '../base-module';
import { createExpectedBearerStringError } from '../../core/sdk-exceptions';
import {createExpectedBearerStringError} from '../../core/sdk-exceptions';
import { ValidateTokenOwnershipResponse } from '../../types';
import { ERC1155ContractABI, ERC721ContractABI } from './ownershipABIs';
import { ErrorCode } from '../../types';

export class UtilsModule extends BaseModule {
/**
Expand All @@ -12,4 +16,64 @@ 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');
}
// Validate DID token
let walletAddress;
try {
await this.sdk.token.validate(didToken);
walletAddress = this.sdk.token.getPublicAddress(didToken);
} catch (e: any) {
// Check if code is malformed token
if (e.code && e.code === 'ERROR_MALFORMED_TOKEN') {
return {
valid: false,
error_code: 'UNAUTHORIZED',
message: 'Invalid DID token: ' + ErrorCode.MalformedTokenError,
};
}
if (e.code === ErrorCode.TokenExpired) {
return {
valid: false,
error_code: 'UNAUTHORIZED',
message: 'Invalid DID token: ' + ErrorCode.TokenExpired,
};
}
throw new Error(e);
}


// 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;
}
4 changes: 2 additions & 2 deletions src/utils/parse-didt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Claim, ParsedDIDToken } from '../types';
import { decodeValue } from './codec';
import { isDIDTClaim } from './type-guards';
import { createMalformedTokenError } from '../core/sdk-exceptions';
import { decodeValue } from './codec';
import { Claim, ParsedDIDToken } from '../types';

interface ParseDIDTokenResult {
raw: [string, string];
Expand Down
17 changes: 9 additions & 8 deletions src/utils/rest.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { RequestInit } from 'node-fetch';
import { createServiceError } from '../core/sdk-exceptions';

import { fetch } from './fetch';
import { createServiceError } from '../core/sdk-exceptions';

interface MagicAPIResponse<TData = {}> {
interface MagicAPIResponse<TData> {
data?: TData;
error_code?: string;
message?: string;
Expand All @@ -12,10 +13,10 @@ interface MagicAPIResponse<TData = {}> {
/**
* Performs a `fetch` to the given URL with the configured `init` object.
*/
async function emitRequest<TResponse extends any = {}>(url: string, init?: RequestInit): Promise<Partial<TResponse>> {
async function emitRequest<TResponse>(url: string, init?: RequestInit): Promise<Partial<TResponse>> {
const json: MagicAPIResponse<TResponse> = await fetch(url, init)
.then(res => res.json())
.catch(err => {
.then((res) => res.json())
.catch((err) => {
throw createServiceError(err);
});

Expand All @@ -29,7 +30,7 @@ async function emitRequest<TResponse extends any = {}>(url: string, init?: Reque
/**
* Generates an encoded URL with query string from a dictionary of values.
*/
function generateQuery<T extends Record<string, string | number | boolean> = {}>(url: string, params?: T) {
function generateQuery<T extends Record<string, string | number | boolean>>(url: string, params?: T) {
let query = '?';
if (params) {
for (const [key, value] of Object.entries(params)) query += `${key}=${value}&`;
Expand All @@ -41,7 +42,7 @@ function generateQuery<T extends Record<string, string | number | boolean> = {}>
/**
* POSTs to Magic's API.
*/
export function post<TBody extends Record<string, string | number | boolean> = {}, TResponse extends any = {}>(
export function post<TBody extends Record<string, string | number | boolean>, TResponse>(
url: string,
secretApiKey: string,
body: TBody,
Expand All @@ -56,7 +57,7 @@ export function post<TBody extends Record<string, string | number | boolean> = {
/**
* GETs from Magic's API.
*/
export function get<TResponse extends any = {}>(url: string, secretApiKey: string, params?: any) {
export function get<TResponse>(url: string, secretApiKey: string, params?: any) {
const urlWithParams = generateQuery(url, params);
return emitRequest<TResponse>(urlWithParams, {
method: 'GET',
Expand Down
12 changes: 12 additions & 0 deletions test/spec/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as MagicAdmin from '../../src/index';

describe('MagicAdmin', () => {
it('should have exports', () => {
expect(MagicAdmin).toEqual(expect.any(Object));
});

it('should not have undefined exports', () => {
for (const k of Object.keys(MagicAdmin))
expect(MagicAdmin).not.toHaveProperty(k, undefined);
});
});
Loading

0 comments on commit 75113c8

Please sign in to comment.