@attestate/delegator2
Version:
An SDK for attestate's delegator2.sol
328 lines (281 loc) • 8.06 kB
JavaScript
import { getAddress } from "@ethersproject/address";
import { verifyTypedData } from "@ethersproject/wallet";
import { splitSignature } from "@ethersproject/bytes";
import log from "./logger.mjs";
const EIP712_DOMAIN = {
name: "kiwinews",
version: "1.0.0",
chainId: 10,
verifyingContract: "0x08b7ecfac2c5754abafb789c84f8fa37c9f088b0",
salt: "0xfe7a9d68e99b6942bb3a36178b251da8bd061c20ed1e795207ae97183b590e5b",
};
export function eligible(allowlist, delegations, address) {
address = getAddress(address);
const allowed0 = allowlist.has(address);
if (allowed0) return address;
const from = delegations[address];
if (!from) return false;
const allowed1 = allowlist.has(from);
if (allowed1) return from;
return false;
}
export function extractLegacyObject(accounts, address) {
if (!accounts[address]) {
throw new Error("Address not found in accounts");
}
let lowestStart, highestEnd;
let hasUnboundedPossession = false;
for (const tokenId in accounts[address].tokens) {
const periods = accounts[address].tokens[tokenId];
for (const period of periods) {
if (
(!lowestStart && period.start) ||
(period.start && lowestStart && period.start < lowestStart)
) {
lowestStart = period.start;
}
if (
(!highestEnd && period.end) ||
(highestEnd && period.end && highestEnd < period.end)
) {
highestEnd = period.end;
}
if (period.start && !period.end) {
hasUnboundedPossession = true;
}
}
}
if (!lowestStart) {
throw new Error(`No start value for address ${address} found`);
}
const { balance } = accounts[address];
if (hasUnboundedPossession) {
return { balance, start: lowestStart };
}
return { balance, start: lowestStart, end: highestEnd };
}
// NOTE: The accounts object must be structured as follows:
//
// {
// [address]: {
// start: <decimal-unix-timestamp>,
// end: <decimal-unix-timestamp>,
// balance: <decimal-number-of-kiwi-passes-held>
// },
// ...
// }
//
// `start` is the timestamp of receiving the first Kiwi Pass NFT and end is the
// unix timestamp when no Kiwi Pass NFT are held anymore.
//
// Additionally, this functio can also validate the eligibly of an address for
// a given historical timestamp. In this case, `validationTime` is set to a
// date in the past.
function legacyEligibleAt(
accounts,
delegations,
address,
validationTime = new Date()
) {
address = getAddress(address);
try {
const account0 = extractLegacyObject(accounts, address);
if (
account0 &&
account0.balance >= 0 &&
(account0.end === undefined || account0.end > validationTime) &&
account0.start < validationTime
) {
return address;
}
} catch (err) {
// NOTE: In the case that we didn't find the address in the accounts object
// we move on to looking into the delegations. We only keep throwing if there
// are other errors.
if (err.message !== "Address not found in accounts") {
throw err;
}
}
const from = delegations[address];
if (!from) return false;
try {
const account1 = extractLegacyObject(accounts, from);
if (
account1 &&
account1.balance >= 1 &&
(account1.end === undefined || account1.end > validationTime) &&
account1.start < validationTime
) {
return from;
}
} catch (err) {
if (err.message !== "Address not found in accounts") {
throw err;
}
return false;
}
return false;
}
// NOTE: The accounts object must be structured as follows:
//
// {
// [address]: {
// tokens: {
// [tokenId] : [{
// start: <decimal-unix-timestamp>,
// end: <decimal-unix-timestamp>,
// },
// //...
// ],
// //...
// },
// balance: <decimal-number-of-kiwi-passes-held>
// },
// ...
// }
export function _eligibleAt(accounts, delegations, params) {
const { validationTime, tokenId } = params;
let { address } = params;
address = getAddress(address);
let account0 = accounts[address];
if (!account0) {
const identity = delegations[address];
if (!(identity in accounts)) return false;
account0 = accounts[identity];
}
if (!(tokenId in account0.tokens)) return false;
const periods = account0.tokens[tokenId];
if (!periods || periods.length === 0) return false;
let isInPeriod = false;
for (let period of periods) {
if (
validationTime >= period.start &&
(validationTime <= period.end || !period.end)
) {
isInPeriod = true;
break;
}
}
return isInPeriod;
}
export function eligibleAt(accounts, delegations, params) {
const { validationTime = new Date(), tokenId } = params;
let { address } = params;
if (!tokenId) {
return legacyEligibleAt(accounts, delegations, address, validationTime);
}
if (!validationTime) {
throw new Error("validationTime property in options input must be set");
}
return _eligibleAt(accounts, delegations, params);
}
export function organize(payloads, domain = EIP712_DOMAIN) {
const delegations = {};
const revoked = new Set();
const froms = new Set();
const tos = new Set();
for (const { data, receipt } of payloads) {
let delegation;
try {
delegation = validate(data, receipt.from, domain);
} catch (err) {
log(`Invalid delegation: ${JSON.stringify(err.message)}`);
continue;
}
const from = getAddress(delegation.from);
const to = getAddress(delegation.to);
const auth = delegation.authorize;
if (froms.has(to)) {
log(`"to" address is already a "from" address: ${to}`);
continue;
}
if (tos.has(from)) {
log(`"from" address is already a "to" address: ${from}`);
continue;
}
if (from === to) {
log(`"from" and "to" are equal: ${from}`);
continue;
}
if (!auth && !delegations[to]) {
log(
`Delegation is a revocation and there is no existing delegation: ${to}`
);
continue;
}
if (!auth) {
revoked.add(to);
delete delegations[to];
log(`Delegation is a revocation: ${to}`);
continue;
}
if (delegations[to]) {
log(`Existing delegation for the "to" address: ${to}`);
continue;
}
if (revoked.has(to)) {
log(`"to" address has been revoked: ${to}`);
continue;
}
delegations[to] = from;
froms.add(from);
tos.add(to);
}
return delegations;
}
export function validate(data, from, domain = EIP712_DOMAIN) {
from = getAddress(from);
// NOTE: We're lower casing the address here before casting it to a checksum
// address as `getAddress` throws on mixed case.
// https://docs.ethers.org/v5/api/utils/address/#utils-getAddress
const to = getAddress(data[2].slice(0, 42).toLowerCase());
const authorize = parseInt(data[2].slice(-1), 16) === 1;
const message = {
from,
authorize,
};
const types = {
Authorization: [
{ name: "from", type: "address" },
{ name: "authorize", type: "bool" },
],
};
const signature = data[0] + data[1].slice(2);
const recoveredTo = getAddress(
verifyTypedData(domain, types, message, signature)
);
if (to !== recoveredTo) {
throw new Error("Recovered address and claimed address aren't equal");
}
return {
from,
to,
authorize,
};
}
export async function create(
signer,
from,
to,
authorize,
domain = EIP712_DOMAIN
) {
from = getAddress(from);
to = getAddress(to);
const message = {
from,
authorize,
};
const types = {
Authorization: [
{ name: "from", type: "address" },
{ name: "authorize", type: "bool" },
],
};
const signature = await signer._signTypedData(domain, types, message);
const { compact } = splitSignature(signature);
const data0 = "0x" + compact.slice(2, 66);
const data1 = "0x" + compact.slice(66);
const flag = authorize ? "1" : "0";
return [data0, data1, `${to}00000000000000000000000${flag}`];
}