Git Product home page Git Product logo

airnode's Introduction

Airnode is a fully-serverless oracle node that is designed specifically for API providers to operate their own oracles.

Documentation

You can find an overview of Airnode in the documentation.

For developers

This is a monorepo managed by Lerna.

Structure

airnode-abi: Encoding and decoding utilities for Airnode according to the Airnode ABI specifications

airnode-adapter: Used for building requests from an Oracle Integration Specification (OIS), executing them, parsing the responses, but also converting and encoding them for on chain purposes

airnode-admin: A package/CLI tool to interact with the Airnode contracts across chains

airnode-deployer: A package/CLI tool to automate Airnode deployment

airnode-examples: A public list of examples showcasing the features of Airnode

airnode-node: The node part of Airnode that allows for connecting multiple blockchains to the rest of the world

airnode-operation: Development and testing utilities for the core parts of Airnode

airnode-protocol: The contracts that implement the Airnode protocols

airnode-utilities: Provides common utilities that are used by multiple Airnode packages

airnode-validator: A package/CLI tool that can be used to validate and convert airnode specification files

Instructions

To install dependencies, run this at the repository root:

yarn run bootstrap

To build all the packages, run this at the repository root:

yarn run build

Airnode packages are cross platform, available as npm packages or docker containers. You should also be able to clone, build and use the packages on any platform. However we do not guarantee that the development only features (e.g. test or examples) will work out of the box.

We heavily recommend using UNIX based systems for development. If you are using Windows, consider WSL.

We use TS project references to see cross-package errors in real time. However, we use ts-node to run our development scripts and it does not support project references at the moment. This means that some of the errors are only shown in the IDE or at build time, not when run using ts-node.

Changelog

We use changesets to manage the changelog for us. What that means for contributors is that you need to add a changeset by running yarn changeset which contains what packages should be bumped, their associated semver bump types and some markdown which will be inserted into changelogs.

A changeset is required to merge a PR if it changes one of the monorepo packages. If you really do not want to include a changeset, you have to generate an empty one by running yarn changeset:empty. Note that a changeset is not required for renovate PRs.

Tip: Add export EDITOR="code --wait" to .bashrc to make it possible to write changelog description in VS Code (you can adapt the configuration for other editor similarly).

Contributing

To request/propose new features, fixes, etc. create an issue. If you wish to contribute to the project, contact us over our Telegram.

airnode's People

Contributors

acenolaza avatar alikonuk1 avatar amarthadan avatar andreogle avatar aquarat avatar arrowana avatar ashar2shahid avatar bbenligiray avatar bdrhn9 avatar crisog avatar cserb avatar dcroote avatar dependabot-preview[bot] avatar dependabot[bot] avatar drgy avatar gpor0 avatar jeebster avatar kurabi1966 avatar martinkolenic avatar metobom avatar mrangus666 avatar omahs avatar renovate[bot] avatar sassmilic avatar siegrift avatar sitch avatar vanshwassan avatar vponline avatar wkande avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

airnode's Issues

Fall back to individual calls if a batch convenience call reverts

One can create a template with a very large parameters field, which would cause the Convenience method to revert due to the exceeding the gas limit, resulting in not being able to retrieve other 9 templates. I'll investigate further.

  • convenience.getTemplates (#184)
  • convenience.getAuthorizationStatuses

Template format changed

Template format used to be

struct Template {
bytes32 providerId;
bytes32 endpointId;
address fulfillAddress;
address errorAddress;
bytes4 fulfillFunctionId;
bytes4 errorFunctionId;
bytes parameters;
}

Now it is

struct Template {
bytes32 providerId;
bytes32 endpointId;
uint256 requesterInd;
address designatedWallet;
address fulfillAddress;
bytes4 fulfillFunctionId;
bytes parameters;
}

getTemplates() from Convenience changed accordingly

function getTemplates(bytes32[] calldata templateIds)
external
view
returns (
bytes32[] memory providerIds,
bytes32[] memory endpointIds,
uint256[] memory requesterInds,
address[] memory designatedWallets,
address[] memory fulfillAddresses,
bytes4[] memory fulfillFunctionIds,
bytes[] memory parameters
);

Types and contract calls need to be updated.

No need for wallet checks

No need to validate that walletInd is not 0 (we don't have walletInd anymore in the first place) or that its balance is more than minBalance.

All code related to walllet checks need to be removed.

API responses can't be cached

Description

API calls may or may not be idempotent so the API provider might not want to make duplicate API calls between serverless function invocations while the transaction is pending. The API response needs to be optionally cached.

The cache should store the following information against the requestId:

  1. The raw value extracted from _path or the error
  2. The timestamp
  3. The status boolean - either successful or failure.

Cache keys need to have an expiration mechanism too.

Before making an API call, the cache should be checked for the requestId. If a value is found, skip making that call and attempt another transaction with that value.

API calls should default to cached if a cache is configured. Otherwise, a new call should be made each serverless invocation.

Questions

  1. How exactly to implement the cache. What AWS service to use etc.
  2. When should the cache key be cleared? If a requestid exists in the cache, but no unfulfilled request is found?

Requests are no longer able errorable

There used to be a method to error a call

function error(
bytes32 requestId,
uint256 errorCode,
address errorAddress,
bytes4 errorFunctionId
)
external
returns(
bool callSuccess,
bytes memory callData
);

Now, the errorCode is returned in statusCode of fulfill:
function fulfill(
bytes32 requestId,
bytes32 providerId,
uint256 statusCode,
bytes32 data,
address fulfillAddress,
bytes4 fulfillFunctionId
)
external
returns(
bool callSuccess,
bytes memory callData
);

All code about erroring needs to be removed. The node should try to call fulfill() and if that reverts, it should call fail() directly.

Need to verify request ID-parameters

Request IDs are derived from request parameters. The derivation changes with type:

Regular:

requestId = keccak256(abi.encode(
noRequests,
templateId,
parameters
));

Short:
requestId = keccak256(abi.encode(
noRequests,
templateId,
parameters
));

Full:
requestId = keccak256(abi.encode(
noRequests,
providerId,
endpointId,
parameters
));

When the node receives a request, it should verify this derivation to ensure that the Ethereum provider didn't tamper with any of the request parameters.

Parallel processing for multiple providers

Description

A user should be able to configure Ethereum providers. This will allow the node to be more resilient if a single provider is down and will also better protection if a node starts reporting bad (or even malicious) data. e.g. The current block number is 99999999.

Step 1

For each configured Ethereum provider, we need to find all of the relevant details at the start of the serverless invocation. This should be done in parallel. We'll need the following information for each provider:

  1. The gas price
  2. Current block number
  3. The configured network
  4. Pending requests

And possibly a few other things. This information should be store against each provider in the state.

Step 2

For unique request, the node should make a single API call. Each of these calls needs to happen in separate serverless functions. The response value should also be extracted in the separate function invocation using the _path parameter. API responses can be very large and we could easily hit the memory limit if multiple large responses get returned to the main function.

Step 3

For each configured Ethereum provider and each response, we make transaction. This means duplicate transactions. I don't think this step needs to be in separate serverless functions, but that is a possibility.

Gas price feed doesn't make sense outside mainnet

Having a gas price data feed on Ropsten (https://github.com/clc-group/airnode/blob/master/packages/node/src/core/ethereum/contracts/gas-price-feed.ts#L6) doesn't make sense because apparently miners don't care about gas prices while mining transactions. Similarly, people on permissioned chains etc. won't be running gas price feeds. Therefore, we can simplify how that works at the node.

The easiest solution I can think of is that the node assumes it's on mainnet, calls the data feed to get the gas price and only use it if it makes sense. This is safe because:
1 - You can't try to deploy a contract on Ropsten to have the exact same address as the mainnet gas price feed address
2 - Even if you did, it's Ropsten
I'll do some research on how this would work and report back.

No need to fetch requester data from Convenience

The node used to get requester data from Convenience:
https://github.com/api3dao/airnode/blob/566fc7b72e1b22486cdc6e87d1e86c27d93ecacd/packages/node/src/core/evm/requests/requester-data.ts
It got requesterId, walletAddress, walletInd, walletBalance, minBalance.

Now, all requests include requesterInd and designatedWallet, there is no separate walletInd, and the minimum balance check is done at an Authorizer.

All code related to fetching requester data should be removed.

Authorization check arguments have changed

Authorization checks used to be for endpoint-client pairs:

function checkAuthorizationStatuses(
bytes32[] calldata endpointIds,
address[] calldata clientAddresses
)
external
view
returns (bool[] memory statuses);

Now they are request specific and have a bunch of other arguments (providerId is not an array!):

function checkAuthorizationStatuses(
bytes32 providerId,
bytes32[] calldata requestIds,
bytes32[] calldata endpointIds,
uint256[] calldata requesterInds,
address[] calldata designatedWallets,
address[] calldata clientAddresses
)
external
view
returns (bool[] memory statuses);

Also note that the non-batch version of this method is also in Convenience (will be used as fallback if the batch version reverts):

function checkAuthorizationStatus(
bytes32 providerId,
bytes32 requestId,
bytes32 endpointId,
uint256 requesterInd,
address designatedWallet,
address clientAddress
)
external
view
returns(bool status);

Require generic Authorizer contracts

We want to provide a set of generic Authorizer contracts for the providers so that they can combine them to implement custom authorization policies.

Verify request and template IDs for integrity

The contracts should derive request IDs (both API and withdrawal) from request-time parameters. For example the short request ID is derived as

requestId = keccak256(abi.encodePacked(
  noRequests,
  templateId,
  parameters
  ));

and emits

emit ShortRequestMade(
  providerId,
  requestId,
  msg.sender,
  noRequests
  templateId,
  parameters
  );

Then, the node can check if templateId or parameters is tampered with by the Ethereum provider by checking if their hash matches the requestId.

The node should do the same thing with templates, i.e., when it fetches a template, it should calculate its hash and check if it matches the template ID.

This should also be done for withdrawal requests

Allow wallet reservation events to be rebroadcast

If a requester reserves makes a transaction to reserve a wallet and the node does not see that event, the requester loses the authorizationDeposit and also gets locked out of being able to have a wallet reserved. The requester should be able to rebroadcast the reservation event without making any deposit for the node to be able to see and handle it.

Note that the node shouldn't serve duplicate events separately.

Withdrawal requests should be sent to the end of the nonce-queue

Currently, withdrawal requests are prioritized and cause other requests made to the respective designated wallet to be ignored.

A malicious provider can't make an Airnode make a withdrawal by faking an event because the fulfillment transaction checks if such a withdrawal request exists on-chain
https://github.com/clc-group/airnode/blob/c1250cbc94f7c3e7c36ffa05edacede0d10eb699/packages/protocol/contracts/ProviderStore.sol#L344
so even if the node attempts to fulfill the faked withdrawal request, the fulfillment will revert.

However, with this scheme, the fake withdrawal event can still be used to put a specific designated wallet of an Airnode out of operation for 1 hour because it will ignore all other requests.

Therefore, withdrawal requests should not cause other requests to be ignored, and should be sent to the back of the nonce-queue.

Requests don't need to be compared deeply

Request IDs are derived from request parameters, for example:

requestId = keccak256(abi.encode(
noRequests,
templateId,
parameters
));

and these IDs are verified when the request is received. After this point, two requests with the same IDs have to be identical

Here, we only need to test for requestId:

const fields = ['id', 'endpointId', 'parameters'];

Multiple Ethereum providers for the Node

The node should attempt to connect to multiple providers on function run in case one or more are down. It should then choose between responding nodes and stick with that provider for the duration of the function.

Maybe it should check for the latest block among responding nodes to determine which provider to use. Not sure what should happen if multiple respond with the same block. Maybe the user should be allowed to rank them in terms of preference?

i.e.

ETHEREUM_PROVIDER_URL=xxx
ETHEREUM_BACKUP_PROVIDER_URL=xxx
ETHEREUM_BACKUP_PROVIDER_2_URL=xxx

(There are probably better names we could use)

Request fulfillment events changed

Request fulfillment events used to be

event FulfillmentSuccessful(
bytes32 indexed providerId,
bytes32 requestId,
bytes32 data
);

event FulfillmentBytesSuccessful(
bytes32 indexed providerId,
bytes32 requestId,
bytes data
);

event FulfillmentErrored(
bytes32 indexed providerId,
bytes32 requestId,
uint256 errorCode
);

event FulfillmentFailed(
bytes32 indexed providerId,
bytes32 requestId
);

Now they are
event ClientRequestFulfilled(
bytes32 indexed providerId,
bytes32 requestId,
uint256 statusCode,
bytes32 data
);

event ClientRequestFulfilledWithBytes(
bytes32 indexed providerId,
bytes32 requestId,
uint256 statusCode,
bytes data
);

event ClientRequestFailed(
bytes32 indexed providerId,
bytes32 requestId
);

The ABI needs to be updated

Allow wallet addresses to make request

Right now, the client contracts announce their (potential) endorser under the public variable requesterId. Wallet addresses can't do the same, so they can't be endorsed. If these are kept it RequesterStore, wallet addresses can also be endorsed and make requests that can be fulfilled with a reserved wallet.

Non-idempotent API operations are not supported

The normal mode of operation doesn't give any guarantees for not calling the API more than once for a single request:
https://api3dao.github.io/api3-docs/pre-alpha/airnode/implementation.html#non-idempotent-operations

The normal mode of operation also doesn't give any guarantees about being absolutely sure that an incoming request is real (and not spoofed for example).

We need to have a mode and guidelines for when the user wants to use non-idempotent operations, and thus only want to call the API when there is an actual request, and make that call only once.

  • The user must only use trusted Ethereum providers. Otherwise, the Ethereum provider can send the user a spoofed request event and have them call the API. The fulfillment of this request will revert, but the main aim of the attack would be to have the user make a call to the API. Managed Ethereum node services largely resolve this issue.
  • The node must wait for enough block confirmations (#41)
  • The node most be configured (by setting a flag for example) to cache API calls (#26). When the node encounters a request, it should first check the cache to see if the call has already been made. If not, it should make the call and create a record at the cache.

Do not require endpoints to be created

Currently, we require a transaction to be made for an endpoint to be created on-chain, essentially to only create an endpoint ID. Instead, we can use a providerId-endpointInd pair to refer to endpoints. This would also be preferable as we would have identical endpointInds across chains (which can be thought together with mirroring the providerId across chains, see #55 ).

Request creation events changed

Request events used to be

event RequestMade(
bytes32 indexed providerId,
bytes32 requestId,
address requester,
bytes32 templateId,
address fulfillAddress,
address errorAddress,
bytes4 fulfillFunctionId,
bytes4 errorFunctionId,
bytes parameters
);

event ShortRequestMade(
bytes32 indexed providerId,
bytes32 requestId,
address requester,
bytes32 templateId,
bytes parameters
);

event FullRequestMade(
bytes32 indexed providerId,
bytes32 requestId,
address requester,
bytes32 endpointId,
address fulfillAddress,
address errorAddress,
bytes4 fulfillFunctionId,
bytes4 errorFunctionId,
bytes parameters
);

Now they are
event ClientRequestCreated(
bytes32 indexed providerId,
bytes32 requestId,
uint256 noRequests,
address requester,
bytes32 templateId,
uint256 requesterInd,
address designatedWallet,
address fulfillAddress,
bytes4 fulfillFunctionId,
bytes parameters
);

event ClientShortRequestCreated(
bytes32 indexed providerId,
bytes32 requestId,
uint256 noRequests,
address requester,
bytes32 templateId,
bytes parameters
);

event ClientFullRequestCreated(
bytes32 indexed providerId,
bytes32 requestId,
uint256 noRequests,
address requester,
bytes32 endpointId,
uint256 requesterInd,
address designatedWallet,
address fulfillAddress,
bytes4 fulfillFunctionId,
bytes parameters
);

Types representing API call requests need to be updated.

Node needs to check for provider data at the start and create a record if necessary

Instead of getting the block number while initalizing the providers, the node should call Convenience to get the block number + provider data:

function getProviderAndBlockNumber(bytes32 providerId)
external
view
returns (
address admin,
string memory xpub,
uint256 blockNumber
);

Before calling this, the node computes providerId off-chain as keccak256(address of path 'm'). If this method returns xpub as '', it means that the record for this provider hasn't been created on this chain yet. Then, the node should call this:
function createProvider(
address admin,
string calldata xpub
)
external
payable
returns (bytes32 providerId);

using the wallet with path m, where admin is from config.json and xpub is derived from the private key.

Note that this method is payable. This is because the provider will have to send some ETH to the wallet with path m for the node to be able to do this. While doing this, the node will send the remaining funds to admin.

Contract tests spend inconsistent amounts of gas

The tests commented out here spend inconsistent amounts of gas
f0ac82c#diff-55b6470a81b9afa68182a49553eedb12

Resetting ganache as below doesn't help

beforeEach(async () => {
  jest.resetModules();
  const ganache = require('ganache-core');
  ...

These tests are not important in particular, but ganache not resetting properly between tests is concerning (assuming the inconsistency is caused by the tests running in changing order).

Withdrawal transactions should estimate gas

There are two withdrawals:

In both cases, we need the node to send balance - txCost to the destination. Here, we should estimate the gas cost to estimate the transaction cost

// Gas cost is 160,076
const estimatedGasCost = await airnode
.connect(masterWallet)
.estimateGas.createProvider(roles.providerAdmin._address, providerXpub, { value: 1 });
// Overestimate a bit
const gasLimit = estimatedGasCost.add(ethers.BigNumber.from(20_000));
const gasPrice = await waffle.provider.getGasPrice();
const txCost = gasLimit.mul(gasPrice);
const masterWalletBalance = await waffle.provider.getBalance(masterWallet.address);
const fundsToSend = masterWalletBalance.sub(txCost);
// Create the provider and send the rest of the master wallet balance along with
// this transaction. Provider admin will receive these funds.
await airnode.connect(masterWallet).createProvider(roles.providerAdmin._address, providerXpub, {
value: fundsToSend,
gasLimit: gasLimit,
gasPrice: gasPrice,
});

// Gas cost is 41,701
const estimatedGasCost = await airnode
.connect(designatedWallet)
.estimateGas.fulfillWithdrawal(
parsedWithdrawalRequestLog.args.withdrawalRequestId,
parsedWithdrawalRequestLog.args.providerId,
parsedWithdrawalRequestLog.args.requesterInd,
parsedWithdrawalRequestLog.args.destination,
{ value: 1 }
);
// Overestimate a bit
const gasLimit = estimatedGasCost.add(ethers.BigNumber.from(20_000));
const gasPrice = await waffle.provider.getGasPrice();
const txCost = gasLimit.mul(gasPrice);
const designatedWalletBalance = await waffle.provider.getBalance(designatedWallet.address);
const fundsToSend = designatedWalletBalance.sub(txCost);
await airnode
.connect(designatedWallet)
.fulfillWithdrawal(
parsedWithdrawalRequestLog.args.withdrawalRequestId,
parsedWithdrawalRequestLog.args.providerId,
parsedWithdrawalRequestLog.args.requesterInd,
parsedWithdrawalRequestLog.args.destination,
{ value: fundsToSend, gasLimit: gasLimit, gasPrice: gasPrice }
);

This (i.e. not hardcoding the gas cost) good for three reasons:

  1. Future forks may change instruction gas costs
  2. The destination may be a payable contract instead of a regular wallet, in which case the gas cost would be variable
  3. Gas cost may vary with the chain

and withdrawals won't be common, so the additional Ethereum call is not significant.

Mirror providerId across chains

Assuming the provider will use a single private key for all chains, we can derive the providerId from that so that they would have identical providerIds in all chains.

ganache-core error message

During tests, ganache-core is giving this error message with no apparent problem

(node:1467) V8: /home/burak/git/airnode/packages/contracts/node_modules/ganache-core/node_modules/rustbn.js/lib/index.asm.js:2 Linking failure in asm.js: Unexpected stdlib member

Wallet designations are being done passively

A requester no longer needs to make a request to have a wallet designated and the node doesn't need to fulfill these.

All code related to wallet designations should be removed.

Fulfillment and failure method arguments changed

Fulfillment and failure arguments used to be

function fulfill(
bytes32 requestId,
bytes32 data,
address fulfillAddress,
bytes4 fulfillFunctionId
)
external
returns(
bool callSuccess,
bytes memory callData
);

function fail(bytes32 requestId)
external;

Now, they are
function fulfill(
bytes32 requestId,
bytes32 providerId,
uint256 statusCode,
bytes32 data,
address fulfillAddress,
bytes4 fulfillFunctionId
)
external
returns(
bool callSuccess,
bytes memory callData
);

function fail(
bytes32 requestId,
bytes32 providerId,
address fulfillAddress,
bytes4 fulfillFunctionId
)
external;

(fail() still doesn't call back fulfillAddress/fulfillFunctionId, it just needs these arguments to check designatedWallet)

This is done for the fulfillment parameters to be checked on-chain. In the previous state, the provider was able to fulfill an existing request using providerId, designatedWallet, fulfillAddress and fulfillFunctionId that are different than the ones defined by the requester.

Calls to these methods need to be updated.

Fulfillment parameters should be checked for

The node should not be able to use any fulfillment parameter (fulfillAddress etc.) they want for a specific request. Their hash should be recorded at request time and checked for during fulfillment.

This would require both fulfill and error to be called with fullfillment and error parameters.

Estimating gas price only using getGasPrice

The initial plan was to use getGasPrice() and also get the gas price from a feed, then use the maximum. The assumption was that getGasPrice() may (will) be below what we want to use, so we can use the gas price feed to ramp up the gas prices temporarily.

This is both not a very elegant solution, and with the increased focus on serving non-mainnet chains, we should move towards a more chain-agnostic solution. My proposed solution is to only depend on getGasPrice(), but use a dead reckoning approach to bump gas prices as needed.

The main idea is to use k * getGasPrice() as the gas price, where k > 1 and increases with currentBlock - requestBlock. The exact coefficient (for example, what is k if the request is 20 blocks old and still not fulfilled?) will be determined based on off-chain statistical analysis of gas prices. This coefficient will be a lot more static than the gas price itself (so it doesn't matter if the gas price is 50 gwei or 500 gwei, k would be 1.5 if the request is 20 blocks old). Then we can hardcode this into config.json. There's also the possibility of keeping this coefficient on-chain (update every week for example), possibly by the API3 DAO or one of its teams.

Need to verify designatedWallet-requesterInd

All requests pass designatedWallet and requesterInd.

When the node receives a request, it should verify that designatedWallet is the address of m/0/${requesterInd} and ignore the request if this is not the case.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.