forked from okTurtles/group-income
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathContract.js
122 lines (113 loc) · 5.13 KB
/
Contract.js
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
'use strict'
import sbp from '~/shared/sbp.js'
import { GIMessage } from '~/shared/GIMessage.js'
// this must not be exported, but instead accessed through 'actionWhitelisted'
const whitelistedSelectors = {}
const sideEffectStacks = {} // [contractID]: Array<*>
export const ACTION_REGEX = /^(([\w.]+)\/([^/]+)\/(?:([^/]+)\/)?)process$/
// ACTION_REGEX.exec('gi.contracts/group/payment/process')
// 0 => 'gi.contracts/group/payment/process'
// 1 => 'gi.contracts/group/payment/'
// 2 => 'gi.contracts'
// 3 => 'group'
// 4 => 'payment'
// TODO: define a flow type for contracts
export function DefineContract (contract: Object) {
const metadata = contract.metadata || { validate () {}, create: () => ({}) }
const getters = contract.getters
sbp('sbp/selectors/register', {
// expose getters for Vuex integration and other conveniences
[`${contract.name}/getters`]: () => getters,
[`${contract.name}/state`]: contract.state,
// there are 2 ways to cause sideEffects to happen: by defining a sideEffect function
// in the contract, or by calling /pushSideEffect with an async SBP call. You can
// also do both.
[`${contract.name}/pushSideEffect`]: function (contractID, asyncSbpCall: Array<*>) {
sideEffectStack(contractID).push(asyncSbpCall)
}
})
for (const action in contract.actions) {
if (action.indexOf(contract.name) !== 0) {
throw new Error(`contract action '${action}' must start with prefix: ${contract.name}`)
}
whitelistedSelectors[`${action}/process`] = true
sbp('sbp/selectors/register', {
[`${action}/create`]: async function (data: Object, contractID: string = null) {
var previousHEAD = null
var state = null
if (contractID) {
state = contract.state(contractID)
previousHEAD = await sbp('backend/latestHash', contractID)
} else if (action !== contract.name) {
throw new Error(`contractID required when calling '${action}/create'`)
}
const meta = metadata.create()
const gProxy = gettersProxy(state, getters)
metadata.validate(meta, { state, ...gProxy, contractID })
contract.actions[action].validate(data, { state, ...gProxy, meta, contractID })
return GIMessage.create(contractID, previousHEAD, undefined, `${action}/process`, data, meta)
},
[`${action}/process`]: function (message: Object, state: Object) {
const { meta, data, contractID } = message
// TODO: optimize so that you're creating a proxy object only when needed
const gProxy = gettersProxy(state, getters)
state = state || contract.state(contractID)
metadata.validate(meta, { state, ...gProxy, contractID })
contract.actions[action].validate(data, { state, ...gProxy, meta, contractID })
contract.actions[action].process(message, { state, ...gProxy })
},
[`${action}/process/sideEffect`]: async function (message: Object, state: ?Object) {
const sideEffects = sideEffectStack(message.contractID)
while (sideEffects.length > 0) {
await sbp(...sideEffects.shift())
}
if (contract.actions[action].sideEffect) {
state = state || contract.state(message.contractID)
const gProxy = gettersProxy(state, getters)
await contract.actions[action].sideEffect(message, { state, ...gProxy })
}
}
})
}
}
function gettersProxy (state: Object, getters: Object) {
const proxyGetters = new Proxy({}, {
get: function (obj, prop) {
return getters[prop](state, proxyGetters)
}
})
return { getters: proxyGetters }
}
function sideEffectStack (contractID: string): Array {
var stack = sideEffectStacks[contractID]
if (!stack) {
sideEffectStacks[contractID] = stack = []
}
return stack
}
export function actionWhitelisted (sel: string): boolean {
return !!whitelistedSelectors[sel]
}
/*
A contract should have the following publicly readable messages:
- contract type
- key management related messages (add/remove authorized write keys, along with their capabilities)
- spoken contract protocol version (note: GIMessage has a 'version' field...)
*/
// TODO: Modify GIMessage to add base protocol ops
// https://github.com/okTurtles/group-income-simple/issues/603
// define a base set of protocol messages that are publicly readable
// OP_CONTRACT - create a contract with a given name (publicly readable),
// and authorized keys, but all other data is encrypted.
// OP_ACTION - an action is applied to the contract. its name and its
// data is encrypted.
// OP_KEY_* - key related ops that determine who can write to this contract
// OP_PROTOCOL_UPGRADE - bump the protocol version, clients that are less
// than this version cannot read or write
// OP_PROP_SET
// OP_PROP_DEL
//
// To make life easier so that you don't have to call hotUpdate and dynamically
// re-register vuex submodules, it might be possible to simply get rid of
// all mutations except for one, "mutate", and have it call an SBP selector
// that's passed in the state, effectively bypassing most of the vuex stuff.