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
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
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,
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');
}
// 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
Loading