Skip to content

Instantly share code, notes, and snippets.

@cellog
Created July 17, 2019 00:21
Show Gist options
  • Save cellog/93c67c86f9fda2b67f7fca5af3af5049 to your computer and use it in GitHub Desktop.
Save cellog/93c67c86f9fda2b67f7fca5af3af5049 to your computer and use it in GitHub Desktop.
Proof-of-Concept - simpler blockchain Handler with no cache
import { PaywallConfig, Locks, Lock, Transactions } from '../../unlockTypes'
import { EventEmitter } from 'events'
import linkKeysToLocks from './linkKeysToLocks'
import { POLLING_INTERVAL } from '../../constants'
type Web3ProviderType = string | Object
export interface WalletServiceType extends EventEmitter {
ready: boolean
connect: (provider: Web3ProviderType) => Promise<void>
getAccount: () => Promise<string | false>
purchaseKey: (
lock: string,
owner: string,
keyPrice: string,
account: any,
data: any,
erc20Address: string | null
) => Promise<string>
}
export interface TransactionDefaults {
to: string
from: string
input: string | null
[key: string]: any
}
export interface KeyResult {
lock: string
owner: string | null
expiration: number
}
export type KeyResults = { [key: string]: KeyResult }
export interface Web3ServiceType extends EventEmitter {
refreshAccountBalance: (account: string) => Promise<string>
getTransaction: (
transactionHash: string,
defaults?: TransactionDefaults
) => void
getLock: (address: string) => Promise<Lock>
getKeyByLockForOwner: (lock: string, owner: string) => Promise<KeyResult>
}
export type unlockNetworks = 1 | 4 | 1984
export interface ConstantsType {
unlockAddress: string
blockTime: number
requiredConfirmations: number
locksmithHost: string
readOnlyProvider: string
defaultNetwork: unlockNetworks
}
export interface BlockchainData {
locks: Locks
account: string | null
balance: string
network: unlockNetworks
}
interface LocksmithTransactionsResult {
transactionHash: string
chain: unlockNetworks
recipient: string
data: string | null
sender: string
for: string
}
interface setupBlockchainHandlerParams {
walletService: WalletServiceType
web3Service: Web3ServiceType
constants: ConstantsType
configuration: PaywallConfig
emitChanges: (data: BlockchainData) => void
}
let config: PaywallConfig
let keys: KeyResults = {}
let locks: Locks = {}
let transactions: Transactions = {}
let account: string | null = null
let network: unlockNetworks
let balance: string = '0'
export function normalizeLockAddress(address: string) {
return address.toLowerCase()
}
export function normalizeAddressKeys(object: { [key: string]: any }) {
return Object.keys(object).reduce(
(newObject: { [key: string]: any }, key) => {
const value = object[key]
newObject[key.toLowerCase()] = value
return newObject
},
{}
)
}
export function setKeys(newKeys: KeyResult[]) {
keys = newKeys.reduce((allKeys: KeyResults, key) => {
key.lock = normalizeLockAddress(key.lock)
allKeys[key.lock] = key
return allKeys
}, {})
}
export function makeDefaultKeys(
lockAddresses: string[],
account: string | null
) {
return lockAddresses.reduce((allKeys: KeyResults, address: string) => {
allKeys[address] = {
lock: address,
owner: account,
expiration: 0,
}
return allKeys
}, {})
}
// assumptions:
// 1. walletService has been "connected" to the Web3ProxyProvider
// 2. config has been validated already
// 3. emitChanges will pass along the updates to the main window
export default async function setupBlockchainHandler({
walletService,
web3Service,
constants,
configuration,
emitChanges,
}: setupBlockchainHandlerParams) {
// take the paywall config, lower-case all the locks
config = configuration
const configLocks = normalizeAddressKeys(configuration.locks)
config.locks = configLocks
// this will be used to filter the keys and transactions we return
const lockAddresses = Object.keys(config.locks)
// first, retrieve the locks
lockAddresses.forEach(address => web3Service.getLock(address))
// this is used to link keys/transactions/locks and send them up to the post office
const propagateChanges = async () => {
const fullLocks = await linkKeysToLocks({
locks,
keys,
transactions,
requiredConfirmations: constants.requiredConfirmations,
})
emitChanges({
locks: fullLocks,
account,
balance,
network,
})
}
// set up defaults
network = constants.defaultNetwork
// poll for account changes
setInterval(() => {
if (!walletService.ready) return
walletService.getAccount()
}, POLLING_INTERVAL)
// the event listeners propagate changes to the main window
// or fetch new data when network or account changes
walletService.on('account.changed', newAccount => {
if (newAccount !== account) {
keys = {}
transactions = {}
getNewData()
}
})
walletService.on(
'account.updated',
(balanceForAccount, { balance: newBalance }) => {
if (balanceForAccount !== account) return
balance = newBalance
propagateChanges()
}
)
walletService.on('network.changed', networkId => {
if (networkId !== network) {
keys = {}
transactions = {}
getNewData()
}
})
web3Service.on('key.updated', (_: any, key: KeyResult) => {
key.lock = normalizeLockAddress(key.lock)
keys[key.lock] = key
propagateChanges()
})
web3Service.on('transaction.updated', (hash, update) => {
transactions[hash] = transactions[hash] || {
blockNumber: Number.MAX_SAFE_INTEGER,
hash,
}
transactions[hash] = {
...transactions[hash],
...update,
}
propagateChanges()
})
web3Service.on('lock.updated', (lockAddress, update) => {
const address = normalizeLockAddress(lockAddress)
locks[address] = locks[address] || { address }
locks[address] = {
...locks[address],
...update,
}
propagateChanges()
})
// this fetches keys/transactions in parallel
async function getNewData() {
// set up keys prior to retrieval
keys = makeDefaultKeys(lockAddresses, account)
if (!account) {
// no account, we only have fake keys and no transactions or balance
transactions = {}
balance = '0'
return
}
// first get keys
lockAddresses.map(address => {
return web3Service.getKeyByLockForOwner(address, account as string)
})
// filter the transactions we request to only include the
// transactions relevant to locks. In most cases this will be
// key purchases
const filterLocks = lockAddresses
.map(lockAddress => `recipient[]=${encodeURIComponent(lockAddress)}`)
.join('&')
const url = `${
constants.locksmithHost
}/transactions?for=${account.toLowerCase()}${
filterLocks ? `&${filterLocks}` : ''
}`
const response = await window.fetch(url)
const result: {
transactions?: LocksmithTransactionsResult[]
} = await response.json()
if (result.transactions) {
Promise.all(
result.transactions
.map(t => ({
hash: t.transactionHash,
network: t.chain,
to: t.recipient,
input: t.data,
from: t.sender,
for: t.for,
}))
.filter(transaction => transaction.network === network)
.map((transaction: TransactionDefaults) => {
// we pass the transaction as defaults if it has input set, so that we can
// parse out the transaction type and other details. If input is not set,
// we can't safely pass the transaction default
web3Service.getTransaction(
transaction.hash,
transaction.input ? transaction : undefined
)
})
)
}
}
return getNewData
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment