forked from passportxyz/passport
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(iam): adding stamp for holding at least 1 NFT
- Loading branch information
Showing
2 changed files
with
155 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// ---- Test subject | ||
import { alchemyGetNFTsUrl, NFTProvider } from "../src/providers/nft"; | ||
|
||
import { RequestPayload } from "@gitcoin/passport-types"; | ||
|
||
// ----- Libs | ||
import axios from "axios"; | ||
|
||
jest.mock("axios"); | ||
|
||
const mockedAxios = axios as jest.Mocked<typeof axios>; | ||
const MOCK_ADDRESS = "0xcF314CE817E25b4F784bC1f24c9A79A525fEC50f"; | ||
const MOCK_ADDRESS_LOWER = MOCK_ADDRESS.toLocaleLowerCase(); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
describe("Attempt verification", function () { | ||
it.only.each([ | ||
[200, 0, false], | ||
[200, 1, true], | ||
[200, 200, true], | ||
[300, 0, false], | ||
[400, 1, false], | ||
[500, 200, false], | ||
])( | ||
" - when status is %p and totalCount is %p valid es expected to be %p", | ||
async (httpStatus, totalCount: number, expectedValid: boolean) => { | ||
(axios.get as jest.Mock).mockImplementation((url) => { | ||
return Promise.resolve({ | ||
status: httpStatus, | ||
data: { | ||
totalCount: totalCount, | ||
ownedNfts: [], | ||
}, | ||
}); | ||
}); | ||
|
||
const nftProvider = new NFTProvider(); | ||
const nftPayload = await nftProvider.verify({ | ||
address: MOCK_ADDRESS, | ||
} as unknown as RequestPayload); | ||
|
||
// Check the request to get the NFTs | ||
expect(axios.get).toHaveBeenCalledTimes(1); | ||
expect(mockedAxios.get).toBeCalledWith(alchemyGetNFTsUrl, { | ||
params: { | ||
withMetadata: "false", | ||
owner: MOCK_ADDRESS_LOWER, | ||
}, | ||
}); | ||
|
||
if (expectedValid) { | ||
expect(nftPayload).toEqual({ | ||
valid: true, | ||
record: { | ||
numTotalNFTs: totalCount.toString(), | ||
}, | ||
}); | ||
} else { | ||
expect(nftPayload).toEqual({ | ||
valid: false, | ||
}); | ||
} | ||
} | ||
); | ||
|
||
it("should return invalid payload when unable to get NFTs (exception thrown)", async () => { | ||
mockedAxios.get.mockImplementation(async () => { | ||
throw "some kind of error"; | ||
}); | ||
|
||
const nftProvider = new NFTProvider(); | ||
const nftPayload = await nftProvider.verify({} as unknown as RequestPayload); | ||
|
||
expect(axios.get).toHaveBeenCalledTimes(1); | ||
expect(nftPayload).toMatchObject({ valid: false }); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// ----- Types | ||
import type { Provider, ProviderOptions } from "../types"; | ||
import type { RequestPayload, VerifiedPayload } from "@gitcoin/passport-types"; | ||
|
||
// ----- Libs | ||
import axios from "axios"; | ||
|
||
// ----- Credential verification | ||
import { getAddress } from "../utils/signer"; | ||
|
||
// Alchemy Api key | ||
export const apiKey = process.env.ALCHEMY_API_KEY; | ||
export const alchemyGetNFTsUrl = `https://eth-mainnet.g.alchemy.com/nft/v2/${apiKey}/getNFTs`; | ||
|
||
type NFTsResponse = { | ||
ownedNfts: []; | ||
totalCount: number; | ||
}; | ||
|
||
// Export a NFT Provider | ||
export class NFTProvider implements Provider { | ||
// Give the provider a type so that we can select it with a payload | ||
type = "NFT"; | ||
|
||
// Options can be set here and/or via the constructor | ||
_options = {}; | ||
|
||
// construct the provider instance with supplied options | ||
constructor(options: ProviderOptions = {}) { | ||
this._options = { ...this._options, ...options }; | ||
} | ||
|
||
// Verify that address defined in the payload owns at least one POAP older than 15 days | ||
async verify(payload: RequestPayload): Promise<VerifiedPayload> { | ||
console.log("geri - verify", payload); | ||
// if a signer is provider we will use that address to verify against | ||
const address = (await getAddress(payload)).toLowerCase(); | ||
|
||
console.log("geri - address"); | ||
let valid = false; | ||
let nftsResponse: NFTsResponse = { | ||
ownedNfts: [], | ||
totalCount: 0, | ||
}; | ||
|
||
try { | ||
console.log("geri - making request"); | ||
const requestResponse = await axios.get(alchemyGetNFTsUrl, { | ||
params: { | ||
withMetadata: "false", | ||
owner: address, | ||
}, | ||
}); | ||
|
||
if (requestResponse.status == 200) { | ||
nftsResponse = requestResponse.data as NFTsResponse; | ||
|
||
console.log("geri - nftsResponse", nftsResponse); | ||
valid = nftsResponse.totalCount > 0; | ||
} | ||
} catch (error) { | ||
// Nothing to do here, valid will remain false | ||
} | ||
|
||
console.log("geri - valid", valid); | ||
return Promise.resolve({ | ||
valid: valid, | ||
record: valid | ||
? { | ||
numTotalNFTs: nftsResponse.totalCount.toString(), | ||
} | ||
: undefined, | ||
}); | ||
} | ||
} |