diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js
new file mode 100644
index 000000000000..b03c5c150561
--- /dev/null
+++ b/modules/riseBidAdapter.js
@@ -0,0 +1,272 @@
+import {registerBidder} from '../src/adapters/bidderFactory.js';
+import * as utils from '../src/utils.js';
+import {VIDEO} from '../src/mediaTypes.js';
+import {config} from '../src/config.js';
+
+const SUPPORTED_AD_TYPES = [VIDEO];
+const BIDDER_CODE = 'rise';
+const BIDDER_VERSION = '4.0.1';
+const TTL = 360;
+const CURRENCY = 'USD';
+const SELLER_ENDPOINT = 'https://hb.yellowblue.io/';
+const MODES = {
+ PRODUCTION: 'hb',
+ TEST: 'hb-test'
+}
+const SUPPORTED_SYNC_METHODS = {
+ IFRAME: 'iframe',
+ PIXEL: 'pixel'
+}
+
+export const spec = {
+ code: BIDDER_CODE,
+ version: BIDDER_VERSION,
+ supportedMediaTypes: SUPPORTED_AD_TYPES,
+ isBidRequestValid: function(bidRequest) {
+ return !!(bidRequest.params.org);
+ },
+ buildRequests: function (bidRequests, bidderRequest) {
+ if (bidRequests.length === 0) {
+ return [];
+ }
+
+ const requests = [];
+
+ bidRequests.forEach(bid => {
+ requests.push(buildVideoRequest(bid, bidderRequest));
+ });
+
+ return requests;
+ },
+ interpretResponse: function({body}) {
+ const bidResponses = [];
+
+ const bidResponse = {
+ requestId: body.requestId,
+ cpm: body.cpm,
+ width: body.width,
+ height: body.height,
+ creativeId: body.requestId,
+ currency: body.currency,
+ netRevenue: body.netRevenue,
+ ttl: body.ttl || TTL,
+ vastXml: body.vastXml,
+ mediaType: VIDEO
+ };
+
+ if (body.adomain && body.adomain.length) {
+ bidResponse.meta = {};
+ bidResponse.meta.advertiserDomains = body.adomain
+ }
+ bidResponses.push(bidResponse);
+
+ return bidResponses;
+ },
+ getUserSyncs: function(syncOptions, serverResponses) {
+ const syncs = [];
+ for (const response of serverResponses) {
+ if (syncOptions.iframeEnabled && response.body.userSyncURL) {
+ syncs.push({
+ type: 'iframe',
+ url: response.body.userSyncURL
+ });
+ }
+ if (syncOptions.pixelEnabled && utils.isArray(response.body.userSyncPixels)) {
+ const pixels = response.body.userSyncPixels.map(pixel => {
+ return {
+ type: 'image',
+ url: pixel
+ }
+ })
+ syncs.push(...pixels)
+ }
+ }
+ return syncs;
+ }
+};
+
+registerBidder(spec);
+
+/**
+ * Get floor price
+ * @param bid {bid}
+ * @returns {Number}
+ */
+function getFloor(bid) {
+ if (!utils.isFn(bid.getFloor)) {
+ return 0;
+ }
+ let floorResult = bid.getFloor({
+ currency: CURRENCY,
+ mediaType: VIDEO,
+ size: '*'
+ });
+ return floorResult.currency === CURRENCY ? floorResult.floor : 0;
+}
+
+/**
+ * Build the video request
+ * @param bid {bid}
+ * @param bidderRequest {bidderRequest}
+ * @returns {Object}
+ */
+function buildVideoRequest(bid, bidderRequest) {
+ const sellerParams = generateParameters(bid, bidderRequest);
+ const {params} = bid;
+ return {
+ method: 'GET',
+ url: getEndpoint(params.testMode),
+ data: sellerParams
+ };
+}
+
+/**
+ * Get the the ad size from the bid
+ * @param bid {bid}
+ * @returns {Array}
+ */
+function getSizes(bid) {
+ if (utils.deepAccess(bid, 'mediaTypes.video.sizes')) {
+ return bid.mediaTypes.video.sizes[0];
+ } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) {
+ return bid.sizes[0];
+ }
+ return [];
+}
+
+/**
+ * Get schain string value
+ * @param schainObject {Object}
+ * @returns {string}
+ */
+function getSupplyChain(schainObject) {
+ if (utils.isEmpty(schainObject)) {
+ return '';
+ }
+ let scStr = `${schainObject.ver},${schainObject.complete}`;
+ schainObject.nodes.forEach((node) => {
+ scStr += '!';
+ scStr += `${getEncodedValIfNotEmpty(node.asi)},`;
+ scStr += `${getEncodedValIfNotEmpty(node.sid)},`;
+ scStr += `${getEncodedValIfNotEmpty(node.hp)},`;
+ scStr += `${getEncodedValIfNotEmpty(node.rid)},`;
+ scStr += `${getEncodedValIfNotEmpty(node.name)},`;
+ scStr += `${getEncodedValIfNotEmpty(node.domain)}`;
+ });
+ return scStr;
+}
+
+/**
+ * Get encoded node value
+ * @param val {string}
+ * @returns {string}
+ */
+function getEncodedValIfNotEmpty(val) {
+ return !utils.isEmpty(val) ? encodeURIComponent(val) : '';
+}
+
+/**
+ * Get preferred user-sync method based on publisher configuration
+ * @param bidderCode {string}
+ * @returns {string}
+ */
+function getAllowedSyncMethod(filterSettings, bidderCode) {
+ const iframeConfigsToCheck = ['all', 'iframe'];
+ const pixelConfigToCheck = 'image';
+ if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) {
+ return SUPPORTED_SYNC_METHODS.IFRAME;
+ }
+ if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) {
+ return SUPPORTED_SYNC_METHODS.PIXEL;
+ }
+}
+
+/**
+ * Check if sync rule is supported
+ * @param syncRule {Object}
+ * @param bidderCode {string}
+ * @returns {boolean}
+ */
+function isSyncMethodAllowed(syncRule, bidderCode) {
+ if (!syncRule) {
+ return false;
+ }
+ const isInclude = syncRule.filter === 'include';
+ const bidders = utils.isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode];
+ return isInclude && utils.contains(bidders, bidderCode);
+}
+
+/**
+ * Get the seller endpoint
+ * @param testMode {boolean}
+ * @returns {string}
+ */
+function getEndpoint(testMode) {
+ return testMode
+ ? SELLER_ENDPOINT + MODES.TEST
+ : SELLER_ENDPOINT + MODES.PRODUCTION;
+}
+
+/**
+ * Generate query parameters for the request
+ * @param bid {bid}
+ * @param bidderRequest {bidderRequest}
+ * @returns {Object}
+ */
+function generateParameters(bid, bidderRequest) {
+ const timeout = config.getConfig('bidderTimeout');
+ const { syncEnabled, filterSettings } = config.getConfig('userSync') || {};
+ const [ width, height ] = getSizes(bid);
+ const { params } = bid;
+ const { bidderCode } = bidderRequest;
+ const domain = window.location.hostname;
+
+ const requestParams = {
+ auction_start: utils.timestamp(),
+ ad_unit_code: utils.getBidIdParameter('adUnitCode', bid),
+ tmax: timeout,
+ width: width,
+ height: height,
+ publisher_id: params.org,
+ floor_price: Math.max(getFloor(bid), params.floorPrice),
+ ua: navigator.userAgent,
+ bid_id: utils.getBidIdParameter('bidId', bid),
+ bidder_request_id: utils.getBidIdParameter('bidderRequestId', bid),
+ transaction_id: utils.getBidIdParameter('transactionId', bid),
+ session_id: utils.getBidIdParameter('auctionId', bid),
+ publisher_name: domain,
+ site_domain: domain,
+ bidder_version: BIDDER_VERSION
+ };
+
+ if (syncEnabled) {
+ const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode);
+ if (allowedSyncMethod) {
+ requestParams.cs_method = allowedSyncMethod;
+ }
+ }
+
+ if (bidderRequest.uspConsent) {
+ requestParams.us_privacy = bidderRequest.uspConsent;
+ }
+
+ if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) {
+ requestParams.gdpr = bidderRequest.gdprConsent.gdprApplies;
+ requestParams.gdpr_consent = bidderRequest.gdprConsent.consentString;
+ }
+
+ if (params.ifa) {
+ requestParams.ifa = params.ifa;
+ }
+
+ if (bid.schain) {
+ requestParams.schain = getSupplyChain(bid.schain);
+ }
+
+ if (bidderRequest && bidderRequest.refererInfo) {
+ requestParams.referrer = utils.deepAccess(bidderRequest, 'refererInfo.referer');
+ requestParams.page_url = config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href');
+ }
+
+ return requestParams;
+}
diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js
new file mode 100644
index 000000000000..b6c2f4fc61a5
--- /dev/null
+++ b/test/spec/modules/riseBidAdapter_spec.js
@@ -0,0 +1,396 @@
+import { expect } from 'chai';
+import { spec } from 'modules/riseBidAdapter.js';
+import { newBidder } from 'src/adapters/bidderFactory.js';
+import { config } from 'src/config.js';
+import { VIDEO } from '../../../src/mediaTypes.js';
+import { deepClone } from 'src/utils.js';
+
+const ENDPOINT = 'https://hb.yellowblue.io/hb';
+const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-test';
+const TTL = 360;
+
+describe('riseAdapter', function () {
+ const adapter = newBidder(spec);
+
+ describe('inherited functions', function () {
+ it('exists and is a function', function () {
+ expect(adapter.callBids).to.exist.and.to.be.a('function');
+ });
+ });
+
+ describe('isBidRequestValid', function () {
+ const bid = {
+ 'bidder': spec.code,
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [['640', '480']],
+ 'params': {
+ 'org': 'jdye8weeyirk00000001'
+ }
+ };
+
+ it('should return true when required params are passed', function () {
+ expect(spec.isBidRequestValid(bid)).to.equal(true);
+ });
+
+ it('should return false when required params are not found', function () {
+ const newBid = Object.assign({}, bid);
+ delete newBid.params;
+ newBid.params = {
+ 'org': null
+ };
+ expect(spec.isBidRequestValid(newBid)).to.equal(false);
+ });
+ });
+
+ describe('buildRequests', function () {
+ const bidRequests = [
+ {
+ 'bidder': spec.code,
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [[640, 480]],
+ 'params': {
+ 'org': 'jdye8weeyirk00000001'
+ },
+ 'bidId': '299ffc8cca0b87',
+ 'bidderRequestId': '1144f487e563f9',
+ 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d',
+ }
+ ];
+
+ const testModeBidRequests = [
+ {
+ 'bidder': spec.code,
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [[640, 480]],
+ 'params': {
+ 'org': 'jdye8weeyirk00000001',
+ 'testMode': true
+ },
+ 'bidId': '299ffc8cca0b87',
+ 'bidderRequestId': '1144f487e563f9',
+ 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d',
+ }
+ ];
+
+ const bidderRequest = {
+ bidderCode: 'rise',
+ }
+
+ it('sends bid request to ENDPOINT via GET', function () {
+ const requests = spec.buildRequests(bidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.url).to.equal(ENDPOINT);
+ expect(request.method).to.equal('GET');
+ }
+ });
+
+ it('sends bid request to test ENDPOINT via GET', function () {
+ const requests = spec.buildRequests(testModeBidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.url).to.equal(TEST_ENDPOINT);
+ expect(request.method).to.equal('GET');
+ }
+ });
+
+ it('should send the correct bid Id', function () {
+ const requests = spec.buildRequests(bidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.data.bid_id).to.equal('299ffc8cca0b87');
+ }
+ });
+
+ it('should send the correct width and height', function () {
+ const requests = spec.buildRequests(bidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.have.property('width', 640);
+ expect(request.data).to.have.property('height', 480);
+ }
+ });
+
+ it('should respect syncEnabled option', function() {
+ config.setConfig({
+ userSync: {
+ syncEnabled: false,
+ filterSettings: {
+ all: {
+ bidders: '*',
+ filter: 'include'
+ }
+ }
+ }
+ });
+ const requests = spec.buildRequests(bidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.not.have.property('cs_method');
+ }
+ });
+
+ it('should respect "iframe" filter settings', function () {
+ config.setConfig({
+ userSync: {
+ syncEnabled: true,
+ filterSettings: {
+ iframe: {
+ bidders: [spec.code],
+ filter: 'include'
+ }
+ }
+ }
+ });
+ const requests = spec.buildRequests(bidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.have.property('cs_method', 'iframe');
+ }
+ });
+
+ it('should respect "all" filter settings', function () {
+ config.setConfig({
+ userSync: {
+ syncEnabled: true,
+ filterSettings: {
+ all: {
+ bidders: [spec.code],
+ filter: 'include'
+ }
+ }
+ }
+ });
+ const requests = spec.buildRequests(bidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.have.property('cs_method', 'iframe');
+ }
+ });
+
+ it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () {
+ config.setConfig({
+ userSync: {
+ syncEnabled: true
+ }
+ });
+ const requests = spec.buildRequests(bidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.have.property('cs_method', 'pixel');
+ }
+ });
+
+ it('should respect total exclusion', function() {
+ config.setConfig({
+ userSync: {
+ syncEnabled: true,
+ filterSettings: {
+ image: {
+ bidders: [spec.code],
+ filter: 'exclude'
+ },
+ iframe: {
+ bidders: [spec.code],
+ filter: 'exclude'
+ }
+ }
+ }
+ });
+ const requests = spec.buildRequests(bidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.not.have.property('cs_method');
+ }
+ });
+
+ it('should have us_privacy param if usPrivacy is available in the bidRequest', function () {
+ const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest);
+ const requests = spec.buildRequests(bidRequests, bidderRequestWithUSP);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.have.property('us_privacy', '1YNN');
+ }
+ });
+
+ it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () {
+ const requests = spec.buildRequests(bidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.not.have.property('us_privacy');
+ }
+ });
+
+ it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () {
+ const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest);
+ const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.not.have.property('gdpr');
+ expect(request.data).to.not.have.property('gdpr_consent');
+ }
+ });
+
+ it('should send the gdpr param if gdprApplies is true in the bidRequest', function () {
+ const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest);
+ const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.have.property('gdpr', true);
+ expect(request.data).to.have.property('gdpr_consent', 'test-consent-string');
+ }
+ });
+
+ it('should have schain param if it is available in the bidRequest', () => {
+ const schain = {
+ ver: '1.0',
+ complete: 1,
+ nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }],
+ };
+ bidRequests[0].schain = schain;
+ const requests = spec.buildRequests(bidRequests, bidderRequest);
+ for (const request of requests) {
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,,,,');
+ }
+ });
+
+ it('should set floor_price to getFloor.floor value if it is greater than params.floorPrice', function() {
+ const bid = deepClone(bidRequests[0]);
+ bid.getFloor = () => {
+ return {
+ currency: 'USD',
+ floor: 3.32
+ }
+ }
+ bid.params.floorPrice = 0.64;
+ const request = spec.buildRequests([bid], bidderRequest)[0];
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.have.property('floor_price', 3.32);
+ });
+
+ it('should set floor_price to params.floorPrice value if it is greater than getFloor.floor', function() {
+ const bid = deepClone(bidRequests[0]);
+ bid.getFloor = () => {
+ return {
+ currency: 'USD',
+ floor: 0.8
+ }
+ }
+ bid.params.floorPrice = 1.5;
+ const request = spec.buildRequests([bid], bidderRequest)[0];
+ expect(request.data).to.be.an('object');
+ expect(request.data).to.have.property('floor_price', 1.5);
+ });
+ });
+
+ describe('interpretResponse', function () {
+ const response = {
+ cpm: 12.5,
+ vastXml: '',
+ width: 640,
+ height: 480,
+ requestId: '21e12606d47ba7',
+ netRevenue: true,
+ currency: 'USD',
+ adomain: ['abc.com']
+ };
+
+ it('should get correct bid response', function () {
+ let expectedResponse = [
+ {
+ requestId: '21e12606d47ba7',
+ cpm: 12.5,
+ width: 640,
+ height: 480,
+ creativeId: '21e12606d47ba7',
+ currency: 'USD',
+ netRevenue: true,
+ ttl: TTL,
+ vastXml: '',
+ mediaType: VIDEO,
+ meta: {
+ advertiserDomains: ['abc.com']
+ }
+ }
+ ];
+ const result = spec.interpretResponse({ body: response });
+ expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0]));
+ });
+ })
+
+ describe('getUserSyncs', function() {
+ const imageSyncResponse = {
+ body: {
+ userSyncPixels: [
+ 'https://image-sync-url.test/1',
+ 'https://image-sync-url.test/2',
+ 'https://image-sync-url.test/3'
+ ]
+ }
+ };
+
+ const iframeSyncResponse = {
+ body: {
+ userSyncURL: 'https://iframe-sync-url.test'
+ }
+ };
+
+ it('should register all img urls from the response', function() {
+ const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]);
+ expect(syncs).to.deep.equal([
+ {
+ type: 'image',
+ url: 'https://image-sync-url.test/1'
+ },
+ {
+ type: 'image',
+ url: 'https://image-sync-url.test/2'
+ },
+ {
+ type: 'image',
+ url: 'https://image-sync-url.test/3'
+ }
+ ]);
+ });
+
+ it('should register the iframe url from the response', function() {
+ const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]);
+ expect(syncs).to.deep.equal([
+ {
+ type: 'iframe',
+ url: 'https://iframe-sync-url.test'
+ }
+ ]);
+ });
+
+ it('should register both image and iframe urls from the responses', function() {
+ const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]);
+ expect(syncs).to.deep.equal([
+ {
+ type: 'iframe',
+ url: 'https://iframe-sync-url.test'
+ },
+ {
+ type: 'image',
+ url: 'https://image-sync-url.test/1'
+ },
+ {
+ type: 'image',
+ url: 'https://image-sync-url.test/2'
+ },
+ {
+ type: 'image',
+ url: 'https://image-sync-url.test/3'
+ }
+ ]);
+ });
+
+ it('should handle an empty response', function() {
+ const syncs = spec.getUserSyncs({ iframeEnabled: true }, []);
+ expect(syncs).to.deep.equal([]);
+ });
+
+ it('should handle when user syncs are disabled', function() {
+ const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]);
+ expect(syncs).to.deep.equal([]);
+ });
+ })
+});