-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbadge.ts
186 lines (154 loc) · 5.54 KB
/
badge.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import { TypedJSON } from 'typedjson';
import { NearNFT, BafBadgeDocument, BafBadge } from './badgeTypes';
import { BadgeDocumentNotFoundError, ContractMethodNotInitializedError, DeserializationError, MalformedResponseError, NFTMediaIntegrityError, NFTReferenceIntegrityError, ReceivedInternalServerError, SerializationError, UnexpectedStatusError } from './errors';
import { v4 as uuid } from 'uuid';
import { sha256 } from './crypto';
const UPLOAD_BADGE_PATH = 'api/uploadDocument';
// fee required for the minter to pay in order to mint an NFT.
// this fee is used by the contract to cover storage costs, as on NEAR
// the contract has to pay "rent" for its storage.
// right now, this is a naively-set "yeah, this should be enough" value
// TODO: (lowest priority) figure out a way to calculate an upper bound on how much it should cost
const MINT_STORAGE_FEE = "20000000000000000000000";
const nftSerializer = new TypedJSON(NearNFT);
const badgeDocumentSerializer = new TypedJSON(BafBadgeDocument);
export async function getBadgeNFT(badgeID: string): Promise<NearNFT> {
if (!window.contract.nft_token) {
throw ContractMethodNotInitializedError('nft_token');
}
const nftRaw = await window.contract.nft_token({ token_id: badgeID });
const nft = nftSerializer.parse(nftRaw);
if (!nft) {
throw DeserializationError('NearNFT', nftRaw);
}
return nft;
}
// return fleek pubic url
export async function uploadBadgeDocument(document: BafBadgeDocument): Promise<string> {
const url = `${UPLOAD_BADGE_PATH}/${document.badgeID}`;
const request = new Request(url, {
method: 'POST',
body: badgeDocumentSerializer.stringify(document)
});
const response = await window.fetch(request);
switch (response.status) {
case 201:
const body = await response.json();
if (!body.url) {
throw MalformedResponseError(UPLOAD_BADGE_PATH, 'response body missing \'url\' property');
}
return body.url;
case 500:
throw ReceivedInternalServerError(UPLOAD_BADGE_PATH);
default:
throw UnexpectedStatusError(url, response.status);
}
}
export async function getBadgeDocument(nft: NearNFT): Promise<BafBadgeDocument> {
const url = nft.metadata.reference;
const response = await window.fetch(url);
switch (response.status) {
case 200:
const body = await response.json();
const document = badgeDocumentSerializer.parse(body);
if (!document) {
throw DeserializationError('BafBadgeDocument', body);
}
return document;
case 404:
throw BadgeDocumentNotFoundError(nft.token_id, url);
default:
throw UnexpectedStatusError(url, response.status);
}
}
export async function getBadge(badgeID: string): Promise<BafBadge> {
const onChain = await getBadgeNFT(badgeID);
await verifyNFTReferenceIntegrity(onChain);
await verifyNFTMediaIntegrity(onChain);
const offChain = await getBadgeDocument(onChain);
return { onChain, offChain }
}
export async function getAllNFTsForOwner(ownerAccountID: string): Promise<NearNFT[]> {
if (!window.contract.nft_tokens_for_owner) {
throw ContractMethodNotInitializedError('nft_token');
}
const nftsRaw = await window.contract.nft_tokens_for_owner({
account_id: ownerAccountID
});
const nfts = nftSerializer.parseAsArray(nftsRaw);
if (!nfts) {
throw DeserializationError('NearNFT', nftsRaw);
}
return nfts;
}
export async function getAllBadgesForOwner(ownerAccountID: string): Promise<BafBadge[]> {
const nfts = await getAllNFTsForOwner(ownerAccountID);
return await Promise.all(
nfts.map(async nft => {
await verifyNFTReferenceIntegrity(nft);
await verifyNFTMediaIntegrity(nft);
const offChain = await getBadgeDocument(nft);
return { onChain: nft, offChain }
})
)
}
export interface BafBadgeCreateArgs {
title: string;
description: string;
offChain: Omit<BafBadgeDocument, "badgeID">;
}
export async function mintBadge(recipientAccountID: string, badgeMediaURL: string, args: BafBadgeCreateArgs): Promise<BafBadge> {
if (!window.contract.nft_mint) {
throw ContractMethodNotInitializedError('mint');
}
const badgeID = uuid().toString();
const document = badgeDocumentSerializer.parse({
...args.offChain,
badgeID
});
if (!document) {
throw SerializationError('BafBadgeDocument', document);
}
const documentPublicUrl = await uploadBadgeDocument(document);
console.log(`uploaded document for badge ${badgeID} to ${documentPublicUrl}`)
const token_metadata = {
title: args.title,
description: args.description,
media: badgeMediaURL,
media_hash: await sha256(badgeMediaURL),
issued_at: (new Date()).toString(),
reference: documentPublicUrl,
reference_hash: await sha256(documentPublicUrl),
}
const nftRaw = await window.contract.nft_mint({
args: {
token_id: badgeID,
token_owner_id: recipientAccountID,
token_metadata,
},
amount: MINT_STORAGE_FEE
});
const nft = nftSerializer.parse(nftRaw);
if (!nft) {
throw DeserializationError('NearNFT', nftRaw);
}
// ! FIXME: this is optimistic. In some circumstances, we should maybe
// ! fetch the document from fleek using documentPublicURL first
// TODO: add an "omptimistic flag" to this method so you can choose. Default to true.
return {
onChain: nft,
offChain: document
}
}
export async function verifyNFTReferenceIntegrity(nft: NearNFT): Promise<void> {
const hash = await sha256(nft.metadata.reference);
if (hash !== nft.metadata.reference_hash) {
throw NFTReferenceIntegrityError(nft.token_id);
}
}
export async function verifyNFTMediaIntegrity(nft: NearNFT): Promise<void> {
const hash = await sha256(nft.metadata.media);
if (hash !== nft.metadata.media_hash) {
throw NFTMediaIntegrityError(nft.token_id);
}
}